From 9e163ee94ad66290577be6bae0190eaef5caaf12 Mon Sep 17 00:00:00 2001 From: arungane Date: Tue, 16 Sep 2025 15:39:07 -0400 Subject: [PATCH 1/4] feat: address book working with search 25 limit --- .../contact-center/cc-components/package.json | 3 +- .../consult-transfer-list-item.tsx | 82 ++- .../consult-transfer-popover.tsx | 542 ++++++++++++++++-- .../src/components/task/task.types.ts | 79 +++ .../contact-center/store/src/store.types.ts | 6 + .../cc/samples-cc-react-app/src/App.scss | 3 +- .../cc/samples-cc-react-app/src/App.tsx | 61 +- yarn.lock | 19 + 8 files changed, 730 insertions(+), 65 deletions(-) diff --git a/packages/contact-center/cc-components/package.json b/packages/contact-center/cc-components/package.json index 436c833e0..bb5242448 100644 --- a/packages/contact-center/cc-components/package.json +++ b/packages/contact-center/cc-components/package.json @@ -35,6 +35,7 @@ "dependencies": { "@momentum-ui/illustrations": "^1.24.0", "@r2wc/react-to-web-component": "2.0.3", + "@tanstack/react-query": "^5.0.0", "@webex/cc-store": "workspace:*", "@webex/cc-ui-logging": "workspace:*" }, @@ -76,4 +77,4 @@ "react": ">=18.3.1", "react-dom": ">=18.3.1" } -} \ No newline at end of file +} diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-list-item.tsx b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-list-item.tsx index aced9fa80..91ce66e17 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-list-item.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-list-item.tsx @@ -15,8 +15,26 @@ const ConsultTransferListComponent: React.FC }; return ( - - + + flex: 1, display: 'flex', flexDirection: 'column', - marginLeft: '8px', + justifyContent: 'center', minWidth: 0, overflow: 'hidden', + paddingRight: '8px', }} > - + {title} {subtitle && ( - + {subtitle} )} - -
- + +
+
diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx index 29078ce05..893e852a5 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx @@ -1,17 +1,27 @@ -import React, {useState} from 'react'; -import {Text, TabListNext, TabNext, ListNext} from '@momentum-ui/react-collaboration'; +import React, {useState, useCallback, useMemo, useRef, useEffect} from 'react'; +import {Text, ListNext, TextInput, Button} from '@momentum-ui/react-collaboration'; import ConsultTransferListComponent from './consult-transfer-list-item'; -import {ConsultTransferPopoverComponentProps} from '../../task.types'; +import {ConsultTransferPopoverComponentProps, AddressBookEntry} from '../../task.types'; import ConsultTransferEmptyState from './consult-transfer-empty-state'; -import { - shouldShowTabs, - isAgentsEmpty, - isQueuesEmpty, - handleTabSelection, - handleAgentSelection, - handleQueueSelection, - getEmptyStateMessage, -} from './call-control-custom.utils'; +import {isAgentsEmpty, isQueuesEmpty, handleAgentSelection, handleQueueSelection} from './call-control-custom.utils'; + +// Types for entry point entries +interface EntryPointEntry { + id: string; + name: string; + number: string; +} + +// Debounce utility function +const debounce = any>(func: T, delay: number): T => { + let timeoutId: NodeJS.Timeout; + return ((...args: any[]) => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => func.apply(null, args), delay); + }) as T; +}; + +type CategoryType = 'Agents' | 'Queues' | 'Dial Number' | 'Entry Point'; const ConsultTransferPopoverComponent: React.FC = ({ heading, @@ -20,18 +30,319 @@ const ConsultTransferPopoverComponent: React.FC { - const [selectedTab, setSelectedTab] = useState('Agents'); - const filteredAgents = buddyAgents; - const filteredQueues = queues; + const [selectedCategory, setSelectedCategory] = useState('Agents'); + const [searchQuery, setSearchQuery] = useState(''); + const loadMoreRef = useRef(null); + + // State for Dial Numbers infinite scroll + const [dialNumbers, setDialNumbers] = useState([]); + const [dialNumbersPage, setDialNumbersPage] = useState(0); + const [hasMoreDialNumbers, setHasMoreDialNumbers] = useState(true); + const [loadingDialNumbers, setLoadingDialNumbers] = useState(false); + + // State for Entry Points infinite scroll + const [entryPoints, setEntryPoints] = useState([]); + const [entryPointsPage, setEntryPointsPage] = useState(0); + const [hasMoreEntryPoints, setHasMoreEntryPoints] = useState(true); + const [loadingEntryPoints, setLoadingEntryPoints] = useState(false); + + // Load initial data for Dial Numbers with proper pagination and search + const loadDialNumbers = useCallback( + async (page = 0, search = '', reset = false) => { + setLoadingDialNumbers(true); + try { + // Build API parameters for pagination and search + const apiParams = { + page, + pageSize: 25, // Use smaller page size for better UX + ...(search && {search}), // Only include search if provided + }; + + logger?.info(`CC-Components: Loading address book entries - page: ${page}, search: "${search}"`); + const response = await getAddressBookEntries(apiParams); + + // Ensure response has expected structure + if (!response || !response.data) { + logger?.error('CC-Components: Invalid response from getAddressBookEntries'); + setDialNumbers([]); + setHasMoreDialNumbers(false); + return; + } + + logger?.info(`CC-Components: Loaded ${response.data.length} address book entries for page ${page}`); + + // Transform the entries to match our expected format + const transformedEntries: AddressBookEntry[] = response.data.map((entry, index) => ({ + id: entry.id || `address-${page}-${index}`, + name: entry.name || 'Unknown', + number: entry.number || '', + organizationId: entry.organizationId, + version: entry.version, + createdTime: entry.createdTime, + lastUpdatedTime: entry.lastUpdatedTime, + })); + + // Update state based on whether this is a reset (new search/category) or append (pagination) + if (reset || page === 0) { + setDialNumbers(transformedEntries); + } else { + setDialNumbers((prev) => [...prev, ...transformedEntries]); + } + + // Update pagination state based on API response + const currentPage = response.meta?.page ?? page; + const totalPages = response.meta?.totalPages ?? 1; + + setDialNumbersPage(currentPage); + setHasMoreDialNumbers(currentPage < totalPages - 1); + + logger?.info( + `CC-Components: Pagination state - current: ${currentPage}, total: ${totalPages}, hasMore: ${currentPage < totalPages - 1}` + ); + } catch (error) { + logger?.error('CC-Components: Error loading dial numbers:', error); + if (reset || page === 0) { + setDialNumbers([]); + } + setHasMoreDialNumbers(false); + } finally { + setLoadingDialNumbers(false); + } + }, + [getAddressBookEntries, logger] + ); + + // Load initial data for Entry Points + const loadEntryPoints = useCallback( + async (page = 0, search = '', reset = false) => { + setLoadingEntryPoints(true); + try { + const entries = await getEntryPoints(); + + // Ensure entries is an array before proceeding + const entriesArray = Array.isArray(entries) ? entries : []; + logger?.info(`CC-Components: Loaded ${entriesArray.length} entry points`); + + // Transform the entries to match our expected format + const transformedEntries: EntryPointEntry[] = entriesArray.map((entry: any, index: number) => ({ + id: entry.id || `entry-${index}`, + name: entry.name || entry.displayName || 'Unknown', + number: entry.number || entry.phoneNumber || entry.extension || '', + })); + + // Apply search filter if provided + let filteredEntries = transformedEntries; + if (search) { + const query = search.toLowerCase(); + filteredEntries = transformedEntries.filter( + (entry) => entry.name.toLowerCase().includes(query) || entry.number.includes(query) + ); + } + + // For now, we'll load all entries since the API doesn't support pagination + // In a real implementation, you might want to implement client-side pagination + setEntryPoints(filteredEntries); + setHasMoreEntryPoints(false); // No pagination for now + setEntryPointsPage(page); + } catch (error) { + logger?.error('Error loading entry points:', error); + setEntryPoints([]); + setHasMoreEntryPoints(false); + } finally { + setLoadingEntryPoints(false); + } + }, + [getEntryPoints, logger] + ); + + // Load next page for current category + const loadNextPage = useCallback(() => { + if (selectedCategory === 'Dial Number' && hasMoreDialNumbers && !loadingDialNumbers) { + loadDialNumbers(dialNumbersPage + 1, searchQuery, false); + } else if (selectedCategory === 'Entry Point' && hasMoreEntryPoints && !loadingEntryPoints) { + loadEntryPoints(entryPointsPage + 1, searchQuery, false); + } + }, [ + selectedCategory, + hasMoreDialNumbers, + hasMoreEntryPoints, + loadingDialNumbers, + loadingEntryPoints, + dialNumbersPage, + entryPointsPage, + searchQuery, + loadDialNumbers, + loadEntryPoints, + ]); + + // Debounced search function + const debouncedSearch = useCallback( + debounce((query: string, category: CategoryType) => { + if (category === 'Dial Number') { + setDialNumbersPage(0); // Reset pagination + setHasMoreDialNumbers(true); // Reset pagination state + loadDialNumbers(0, query, true); + } else if (category === 'Entry Point') { + setEntryPointsPage(0); // Reset pagination + setHasMoreEntryPoints(true); // Reset pagination state + loadEntryPoints(0, query, true); + } + }, 300), + [loadDialNumbers, loadEntryPoints] + ); + + // Handle search input change + const handleSearchChange = useCallback( + (value: string) => { + setSearchQuery(value); + + // For categories that support API search, use debounced search + if (selectedCategory === 'Dial Number' || selectedCategory === 'Entry Point') { + debouncedSearch(value, selectedCategory); + } + // For Agents and Queues, trigger fresh API calls if search functions are available + else if (selectedCategory === 'Agents' && getBuddyAgents) { + // Reset search query and let the filtered memoized result handle client-side filtering + // In future, this could be enhanced to use getBuddyAgents(value) for server-side search + } else if (selectedCategory === 'Queues' && getQueues) { + // Reset search query and let the filtered memoized result handle client-side filtering + // In future, this could be enhanced to use getQueues(value) for server-side search + } + }, + [selectedCategory, debouncedSearch, getBuddyAgents, getQueues] + ); + + // Handle category change + const handleCategoryChange = useCallback( + (category: CategoryType) => { + console.log(`Category change attempted: ${category}`); // Console log for immediate debugging + logger?.info(`CC-Components: Category changed to: ${category}`); + setSelectedCategory(category); + setSearchQuery(''); + + // Reset pagination state for all categories + setDialNumbersPage(0); + setHasMoreDialNumbers(true); + setEntryPointsPage(0); + setHasMoreEntryPoints(true); + + // Clear existing data and let useEffect handle loading + if (category === 'Dial Number') { + setDialNumbers([]); // Clear existing data + } else if (category === 'Entry Point') { + setEntryPoints([]); // Clear existing data + } + }, + [logger] + ); + + // Alternative click handlers for individual radio buttons + const handleAgentsClick = useCallback(() => { + console.log('Agents clicked directly'); + handleCategoryChange('Agents'); + }, [handleCategoryChange]); + + const handleQueuesClick = useCallback(() => { + console.log('Queues clicked directly'); + handleCategoryChange('Queues'); + }, [handleCategoryChange]); + + const handleDialNumberClick = useCallback(() => { + console.log('Dial Number clicked directly'); + handleCategoryChange('Dial Number'); + }, [handleCategoryChange]); + + const handleEntryPointClick = useCallback(() => { + console.log('Entry Point clicked directly'); + handleCategoryChange('Entry Point'); + }, [handleCategoryChange]); + + // Intersection Observer for infinite scroll + useEffect(() => { + const loadMoreElement = loadMoreRef.current; + if (!loadMoreElement) return; + + const observer = new IntersectionObserver( + (entries) => { + const [entry] = entries; + if (entry.isIntersecting) { + loadNextPage(); + } + }, + {threshold: 1.0} + ); + + observer.observe(loadMoreElement); + + return () => { + observer.unobserve(loadMoreElement); + }; + }, [loadNextPage]); + + // Load initial data when component mounts or category changes + useEffect(() => { + if (selectedCategory === 'Dial Number' && !loadingDialNumbers) { + // Always load if we don't have data OR if we just switched to this category + if (dialNumbers.length === 0) { + logger?.info('CC-Components: Loading dial numbers for first time or after category switch'); + loadDialNumbers(0, '', true); + } + } else if (selectedCategory === 'Entry Point' && !loadingEntryPoints) { + // Always load if we don't have data OR if we just switched to this category + if (entryPoints.length === 0) { + logger?.info('CC-Components: Loading entry points for first time or after category switch'); + loadEntryPoints(0, '', true); + } + } + }, [ + selectedCategory, + dialNumbers.length, + entryPoints.length, + loadDialNumbers, + loadEntryPoints, + loadingDialNumbers, + loadingEntryPoints, + logger, + ]); + + // Filter agents based on search (client-side) + const filteredAgents = useMemo(() => { + if (selectedCategory !== 'Agents' || !searchQuery) { + return buddyAgents; + } + const query = searchQuery.toLowerCase(); + return buddyAgents.filter((agent) => agent.agentName.toLowerCase().includes(query)); + }, [buddyAgents, searchQuery, selectedCategory]); + + // Filter queues based on search (client-side for now) + const filteredQueues = useMemo(() => { + if (selectedCategory !== 'Queues' || !searchQuery || !queues) { + return queues || []; + } + const query = searchQuery.toLowerCase(); + return queues.filter((queue) => queue.name.toLowerCase().includes(query)); + }, [queues, searchQuery, selectedCategory]); const noAgents = isAgentsEmpty(filteredAgents); const noQueues = isQueuesEmpty(filteredQueues); - const showTabs = shouldShowTabs(filteredAgents, filteredQueues); + const noDialNumbers = dialNumbers.length === 0; + const noEntryPoints = entryPoints.length === 0; - const renderList = (items, getKey, getTitle, handleSelect) => ( + const renderList = ( + items: T[], + getKey: (item: T) => string, + getTitle: (item: T) => string, + handleSelect: (id: string, name: string, number?: string) => void + ) => ( {items.map((item) => (
handleSelect(getKey(item), getTitle(item))} + onButtonPress={() => { + const id = getKey(item); + const name = getTitle(item); + const number = item.number; + handleSelect(id, name, number); + }} logger={logger} />
))} {items.length === 0 && ( - No {selectedTab.toLowerCase()} found + No {selectedCategory.toLowerCase()} found )}
); + const hasAnyData = !noAgents || !noQueues || !noDialNumbers || !noEntryPoints; + + // Debug logging + useEffect(() => { + logger?.info(`CC-Components: Debug - selectedCategory: ${selectedCategory}`); + logger?.info(`CC-Components: Debug - buddyAgents: ${JSON.stringify(buddyAgents?.slice(0, 2) || [])}`); + logger?.info(`CC-Components: Debug - queues: ${JSON.stringify(queues?.slice(0, 2) || [])}`); + logger?.info(`CC-Components: Debug - dialNumbers: ${JSON.stringify(dialNumbers.slice(0, 2))}`); + logger?.info(`CC-Components: Debug - entryPoints: ${JSON.stringify(entryPoints.slice(0, 2))}`); + logger?.info(`CC-Components: Debug - hasAnyData: ${hasAnyData}`); + }, [selectedCategory, buddyAgents, queues, dialNumbers, entryPoints, hasAnyData, logger]); + return (
{heading} - {/* Only show tabs if at least one list is available */} - {showTabs && ( - { - handleTabSelection(key as string, setSelectedTab, logger); - }} + {/* Global Search Input */} +
+ handleSearchChange(value)} + clearAriaLabel="Clear search" + aria-labelledby="consult-search-label" + style={{width: '100%'}} + /> +
+ + {/* Category Selection Buttons */} +
+ + + + +
+ + {/* If no data available, show empty state */} + {!hasAnyData && } + + {/* Render content based on selected category */} + {selectedCategory === 'Agents' && noAgents && ( + )} - {/* If both are empty, show the big empty state */} - {!showTabs && } + {selectedCategory === 'Queues' && noQueues && ( + + )} - {/* If agents tab is selected and empty */} - {showTabs && selectedTab === 'Agents' && noAgents && ( - + {selectedCategory === 'Dial Number' && noDialNumbers && ( + )} - {/* If queues tab is selected and empty */} - {showTabs && selectedTab === 'Queues' && noQueues && ( - + {selectedCategory === 'Entry Point' && noEntryPoints && ( + )} {/* Render lists if not empty */} - {showTabs && - selectedTab === 'Agents' && + {selectedCategory === 'Agents' && !noAgents && renderList( filteredAgents, @@ -113,8 +472,7 @@ const ConsultTransferPopoverComponent: React.FC + {renderList( + dialNumbers, + (entry) => entry.id, + (entry) => entry.name, + (id, name, number) => { + if (onDialNumberSelect && number) { + onDialNumberSelect(id, name, number); + } + } + )} + {/* Infinite scroll trigger */} + {hasMoreDialNumbers && ( +
+ {loadingDialNumbers ? ( + + Loading more dial numbers... + + ) : ( + + Scroll to load more + + )} +
+ )} +
+ )} + + {selectedCategory === 'Entry Point' && !noEntryPoints && ( +
+ {renderList( + entryPoints, + (entry) => entry.id, + (entry) => entry.name, + (id, name, number) => { + if (onEntryPointSelect && number) { + onEntryPointSelect(id, name, number); + } + } + )} + {/* Infinite scroll trigger */} + {hasMoreEntryPoints && ( +
+ {loadingEntryPoints ? ( + + Loading more entry points... + + ) : ( + + Scroll to load more + + )} +
+ )} +
+ )}
); }; diff --git a/packages/contact-center/cc-components/src/components/task/task.types.ts b/packages/contact-center/cc-components/src/components/task/task.types.ts index f8a94bac9..755aa5d31 100644 --- a/packages/contact-center/cc-components/src/components/task/task.types.ts +++ b/packages/contact-center/cc-components/src/components/task/task.types.ts @@ -502,8 +502,14 @@ export interface ConsultTransferPopoverComponentProps { queues?: ContactServiceQueue[]; onAgentSelect?: (agentId: string, agentName: string) => void; onQueueSelect?: (queueId: string, queueName: string) => void; + onDialNumberSelect?: (id: string, name: string, number: string) => void; + onEntryPointSelect?: (id: string, name: string, number: string) => void; allowConsultToQueue: boolean; logger: ILogger; + getAddressBookEntries: (params?: AddressBookEntrySearchParams) => Promise; + getEntryPoints: () => Promise; + getBuddyAgents?: (searchTerm?: string) => Promise; + getQueues?: (searchTerm?: string) => Promise; } /** @@ -653,3 +659,76 @@ export interface TimerUIState { iconName: string; formattedTime: string; } + +/** + * AddressBook API Types + */ + +export interface AddressBookEntrySearchParams { + /** Address book ID (optional, uses agent's address book if not provided) */ + addressBookId?: string; + + /** Filter criteria using RSQL syntax */ + filter?: string; + + /** Attributes to be returned */ + attributes?: string; + + /** Search keyword for name and number fields */ + search?: string; + + /** Page number (starts from 0) */ + page?: number; + + /** Number of items per page (default: 100) */ + pageSize?: number; +} + +export interface AddressBookEntry { + /** Unique identifier for the entry */ + id: string; + + /** Organization ID this entry belongs to */ + organizationId?: string; + + /** Version of the entry */ + version?: number; + + /** Name of the entry */ + name: string; + + /** Phone number for the entry */ + number: string; + + /** Creation timestamp in epoch millis */ + createdTime?: number; + + /** Last updated timestamp in epoch millis */ + lastUpdatedTime?: number; +} + +export interface AddressBookEntriesResponse { + /** Array of address book entries */ + data: AddressBookEntry[]; + + /** Pagination metadata */ + meta: { + /** Organization ID */ + orgid?: string; + + /** Current page number */ + page?: number; + + /** Page size for current data set */ + pageSize?: number; + + /** Number of pages */ + totalPages?: number; + + /** Total number of items */ + totalRecords?: number; + + /** Map of pagination links */ + links?: Record; + }; +} diff --git a/packages/contact-center/store/src/store.types.ts b/packages/contact-center/store/src/store.types.ts index f85239ed1..b62b1c91c 100644 --- a/packages/contact-center/store/src/store.types.ts +++ b/packages/contact-center/store/src/store.types.ts @@ -42,6 +42,12 @@ interface IContactCenter { agentId: string; }; setAgentState(data: StateChange): Promise; + addressBook: { + getEntries: () => Promise; + }; + entryPoints: { + getEntryPoints: () => Promise; + }; } // To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 type IWebex = { diff --git a/widgets-samples/cc/samples-cc-react-app/src/App.scss b/widgets-samples/cc/samples-cc-react-app/src/App.scss index 9d0c09f0a..b28ff480d 100644 --- a/widgets-samples/cc/samples-cc-react-app/src/App.scss +++ b/widgets-samples/cc/samples-cc-react-app/src/App.scss @@ -16,6 +16,7 @@ .webexTheme { padding: 1rem; + padding-bottom: 4rem; /* Add extra bottom padding to prevent last component from being hidden */ height: 100%; color: var(--mds-color-theme-button-primary-normal); background-color: var(--mds-color-theme-inverted-text-primary-normal); @@ -198,4 +199,4 @@ iframe { border: none; font-size: 16px; cursor: pointer; -} \ No newline at end of file +} diff --git a/widgets-samples/cc/samples-cc-react-app/src/App.tsx b/widgets-samples/cc/samples-cc-react-app/src/App.tsx index af134eff5..c0bb06dc0 100644 --- a/widgets-samples/cc/samples-cc-react-app/src/App.tsx +++ b/widgets-samples/cc/samples-cc-react-app/src/App.tsx @@ -9,7 +9,7 @@ import { store, OutdialCall, } from '@webex/cc-widgets'; -import {StationLogoutResponse} from '@webex/contact-center'; +import {StationLogoutResponse, BuddyAgents, BuddyDetails} from '@webex/contact-center'; import {ERROR_TRIGGERING_IDLE_CODES} from '@webex/cc-store'; import Webex from 'webex'; import { @@ -26,6 +26,8 @@ import {PopoverNext} from '@momentum-ui/react-collaboration'; import './App.scss'; import {observer} from 'mobx-react-lite'; import EngageWidget from './EngageWidget'; +// Direct import of the ConsultTransferPopoverComponent +import ConsultTransferPopoverComponent from '../../../../packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover'; // This is not to be included to a production app. // Have added here for debugging purposes @@ -39,6 +41,7 @@ const defaultWidgets = { callControl: true, callControlCAD: true, outdialCall: true, + consultTransferPopover: true, }; window['AGENTX_SERVICE'] = {}; // Make it available in the window object for global access for engage widgets @@ -381,6 +384,8 @@ function App() { switch (widget) { case 'callControlCAD': return 'Call Controls with Call Associated Data (CAD)'; + case 'consultTransferPopover': + return 'Consult Transfer Popover'; default: return widget.charAt(0).toUpperCase() + widget.slice(1).replace(/([A-Z])/g, ' $1'); } @@ -828,6 +833,60 @@ function App() { )} {selectedWidgets.outdialCall && } + {selectedWidgets.consultTransferPopover && ( +
+
+
+ Consult Transfer Popover + { + console.log('Agent selected:', agentId, agentName); + }} + onQueueSelect={(queueId, queueName) => { + console.log('Queue selected:', queueId, queueName); + }} + onDialNumberSelect={(id, name, number) => { + console.log('Dial number selected:', id, name, number); + }} + onEntryPointSelect={(id, name, number) => { + console.log('Entry point selected:', id, name, number); + }} + allowConsultToQueue={true} + logger={store.logger} + getAddressBookEntries={async (params) => { + return await store.cc.addressBook.getEntries(params); + }} + getEntryPoints={async () => { + return await store.cc.entryPoints.getEntryPoints(); + }} + getBuddyAgents={async (searchTerm?: string): Promise => { + try { + // Create the BuddyAgents data object with required mediaType + const buddyAgentsData: BuddyAgents = { + mediaType: 'telephony', // Required field + ...(searchTerm && {state: 'Available'}), // Use state filter when search term is provided + }; + const response = await store.cc.getBuddyAgents(buddyAgentsData); + // Return the buddy agents array from the response + if (response && typeof response === 'object' && 'agentList' in response) { + return response.agentList || []; + } + return []; + } catch (error) { + console.error('Error fetching buddy agents:', error); + return []; + } + }} + getQueues={async (searchTerm?: string) => { + return await store.cc.getQueues(searchTerm); + }} + /> +
+
+
+ )} )} diff --git a/yarn.lock b/yarn.lock index 5717dcc00..24a6df7cb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7485,6 +7485,24 @@ __metadata: languageName: node linkType: hard +"@tanstack/query-core@npm:5.87.4": + version: 5.87.4 + resolution: "@tanstack/query-core@npm:5.87.4" + checksum: 10c0/00898c96d199ba8f0cd2004b76da8e5e3355b97473a6c16dd13de65f5063c4f2abfae64e9f32fc395557c2f446af09e1646a82f62992256fdda6062c9399c2a3 + languageName: node + linkType: hard + +"@tanstack/react-query@npm:^5.0.0": + version: 5.87.4 + resolution: "@tanstack/react-query@npm:5.87.4" + dependencies: + "@tanstack/query-core": "npm:5.87.4" + peerDependencies: + react: ^18 || ^19 + checksum: 10c0/2dbfba2eabc21d9c7a6683ff8858fa2cc896d2e4319a2e4e1cb4b794fc4b7eb5b16da2b72ba98468c7bebdb035c73ad75f5d7abcc5cae64a9bcbcc728131193d + languageName: node + linkType: hard + "@tanstack/virtual-core@npm:3.13.2": version: 3.13.2 resolution: "@tanstack/virtual-core@npm:3.13.2" @@ -9403,6 +9421,7 @@ __metadata: "@eslint/js": "npm:^9.20.0" "@momentum-ui/illustrations": "npm:^1.24.0" "@r2wc/react-to-web-component": "npm:2.0.3" + "@tanstack/react-query": "npm:^5.0.0" "@testing-library/dom": "npm:10.4.0" "@testing-library/jest-dom": "npm:6.6.2" "@testing-library/react": "npm:16.0.1" From 18d437b8909b12a428a149277a59890c2ef793b3 Mon Sep 17 00:00:00 2001 From: arungane Date: Wed, 17 Sep 2025 21:23:09 -0400 Subject: [PATCH 2/4] feat(contact-center): final version of working entry point , queue , and address book --- .../consult-transfer-popover.tsx | 454 ++++++++---------- .../CallControlCustom/usePaginatedData.ts | 97 ++++ .../src/components/task/task.types.ts | 139 +++++- .../store/src/storeEventsWrapper.ts | 29 -- packages/contact-center/task/src/helper.ts | 27 -- .../cc/samples-cc-react-app/src/App.tsx | 8 +- 6 files changed, 439 insertions(+), 315 deletions(-) create mode 100644 packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/usePaginatedData.ts diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx index 893e852a5..e8cafd24c 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx @@ -1,24 +1,27 @@ import React, {useState, useCallback, useMemo, useRef, useEffect} from 'react'; import {Text, ListNext, TextInput, Button} from '@momentum-ui/react-collaboration'; import ConsultTransferListComponent from './consult-transfer-list-item'; -import {ConsultTransferPopoverComponentProps, AddressBookEntry} from '../../task.types'; +import { + ConsultTransferPopoverComponentProps, + AddressBookEntry, + EntryPointEntry, + QueueEntry, + BuddyDetails, +} from '../../task.types'; import ConsultTransferEmptyState from './consult-transfer-empty-state'; -import {isAgentsEmpty, isQueuesEmpty, handleAgentSelection, handleQueueSelection} from './call-control-custom.utils'; - -// Types for entry point entries -interface EntryPointEntry { - id: string; - name: string; - number: string; -} +import {isAgentsEmpty, handleAgentSelection, handleQueueSelection} from './call-control-custom.utils'; +import {usePaginatedData} from './usePaginatedData'; // Debounce utility function -const debounce = any>(func: T, delay: number): T => { +const debounce = void>( + func: T, + delay: number +): ((...args: Parameters) => void) => { let timeoutId: NodeJS.Timeout; - return ((...args: any[]) => { + return (...args: Parameters) => { clearTimeout(timeoutId); - timeoutId = setTimeout(() => func.apply(null, args), delay); - }) as T; + timeoutId = setTimeout(() => func(...args), delay); + }; }; type CategoryType = 'Agents' | 'Queues' | 'Dial Number' | 'Entry Point'; @@ -26,8 +29,6 @@ type CategoryType = 'Agents' | 'Queues' | 'Dial Number' | 'Entry Point'; const ConsultTransferPopoverComponent: React.FC = ({ heading, buttonIcon, - buddyAgents, - queues, onAgentSelect, onQueueSelect, onDialNumberSelect, @@ -43,228 +44,181 @@ const ConsultTransferPopoverComponent: React.FC(null); - // State for Dial Numbers infinite scroll - const [dialNumbers, setDialNumbers] = useState([]); - const [dialNumbersPage, setDialNumbersPage] = useState(0); - const [hasMoreDialNumbers, setHasMoreDialNumbers] = useState(true); - const [loadingDialNumbers, setLoadingDialNumbers] = useState(false); - - // State for Entry Points infinite scroll - const [entryPoints, setEntryPoints] = useState([]); - const [entryPointsPage, setEntryPointsPage] = useState(0); - const [hasMoreEntryPoints, setHasMoreEntryPoints] = useState(true); - const [loadingEntryPoints, setLoadingEntryPoints] = useState(false); - - // Load initial data for Dial Numbers with proper pagination and search - const loadDialNumbers = useCallback( - async (page = 0, search = '', reset = false) => { - setLoadingDialNumbers(true); - try { - // Build API parameters for pagination and search - const apiParams = { - page, - pageSize: 25, // Use smaller page size for better UX - ...(search && {search}), // Only include search if provided - }; - - logger?.info(`CC-Components: Loading address book entries - page: ${page}, search: "${search}"`); - const response = await getAddressBookEntries(apiParams); - - // Ensure response has expected structure - if (!response || !response.data) { - logger?.error('CC-Components: Invalid response from getAddressBookEntries'); - setDialNumbers([]); - setHasMoreDialNumbers(false); - return; - } - - logger?.info(`CC-Components: Loaded ${response.data.length} address book entries for page ${page}`); - - // Transform the entries to match our expected format - const transformedEntries: AddressBookEntry[] = response.data.map((entry, index) => ({ - id: entry.id || `address-${page}-${index}`, - name: entry.name || 'Unknown', - number: entry.number || '', - organizationId: entry.organizationId, - version: entry.version, - createdTime: entry.createdTime, - lastUpdatedTime: entry.lastUpdatedTime, - })); - - // Update state based on whether this is a reset (new search/category) or append (pagination) - if (reset || page === 0) { - setDialNumbers(transformedEntries); - } else { - setDialNumbers((prev) => [...prev, ...transformedEntries]); - } - - // Update pagination state based on API response - const currentPage = response.meta?.page ?? page; - const totalPages = response.meta?.totalPages ?? 1; - - setDialNumbersPage(currentPage); - setHasMoreDialNumbers(currentPage < totalPages - 1); - - logger?.info( - `CC-Components: Pagination state - current: ${currentPage}, total: ${totalPages}, hasMore: ${currentPage < totalPages - 1}` - ); - } catch (error) { - logger?.error('CC-Components: Error loading dial numbers:', error); - if (reset || page === 0) { - setDialNumbers([]); - } - setHasMoreDialNumbers(false); - } finally { - setLoadingDialNumbers(false); - } - }, - [getAddressBookEntries, logger] + // State for Buddy Agents + const [buddyAgents, setBuddyAgents] = useState([]); + const [loadingBuddyAgents, setLoadingBuddyAgents] = useState(false); + + const { + data: dialNumbers, + page: dialNumbersPage, + hasMore: hasMoreDialNumbers, + loading: loadingDialNumbers, + loadData: loadDialNumbers, + reset: resetDialNumbers, + } = usePaginatedData( + getAddressBookEntries, + (entry, page, index) => ({ + id: entry.id || `address-${page}-${index}`, + name: entry.name || 'Unknown', + number: entry.number || '', + organizationId: entry.organizationId, + version: entry.version, + createdTime: entry.createdTime, + lastUpdatedTime: entry.lastUpdatedTime, + }), + logger, + 'Dial Numbers' ); - // Load initial data for Entry Points - const loadEntryPoints = useCallback( - async (page = 0, search = '', reset = false) => { - setLoadingEntryPoints(true); - try { - const entries = await getEntryPoints(); - - // Ensure entries is an array before proceeding - const entriesArray = Array.isArray(entries) ? entries : []; - logger?.info(`CC-Components: Loaded ${entriesArray.length} entry points`); - - // Transform the entries to match our expected format - const transformedEntries: EntryPointEntry[] = entriesArray.map((entry: any, index: number) => ({ - id: entry.id || `entry-${index}`, - name: entry.name || entry.displayName || 'Unknown', - number: entry.number || entry.phoneNumber || entry.extension || '', - })); - - // Apply search filter if provided - let filteredEntries = transformedEntries; - if (search) { - const query = search.toLowerCase(); - filteredEntries = transformedEntries.filter( - (entry) => entry.name.toLowerCase().includes(query) || entry.number.includes(query) - ); - } + const { + data: entryPoints, + page: entryPointsPage, + hasMore: hasMoreEntryPoints, + loading: loadingEntryPoints, + loadData: loadEntryPoints, + reset: resetEntryPoints, + } = usePaginatedData( + getEntryPoints, + (entry, page, index) => ({ + id: entry.id || `entry-${page}-${index}`, + name: entry.name || entry.displayName || 'Unknown', + number: entry.number || entry.phoneNumber || entry.extension || '', + organizationId: entry.organizationId, + }), + logger, + 'Entry Points' + ); - // For now, we'll load all entries since the API doesn't support pagination - // In a real implementation, you might want to implement client-side pagination - setEntryPoints(filteredEntries); - setHasMoreEntryPoints(false); // No pagination for now - setEntryPointsPage(page); - } catch (error) { - logger?.error('Error loading entry points:', error); - setEntryPoints([]); - setHasMoreEntryPoints(false); - } finally { - setLoadingEntryPoints(false); - } - }, - [getEntryPoints, logger] + const { + data: queues, + page: queuesPage, + hasMore: hasMoreQueues, + loading: loadingQueues, + loadData: loadQueues, + reset: resetQueues, + } = usePaginatedData( + getQueues, + (entry, page, index) => ({ + id: entry.id || `queue-${page}-${index}`, + name: entry.name || 'Unknown Queue', + description: entry.description, + organizationId: entry.organizationId, + type: entry.type, + status: entry.status, + }), + logger, + 'Queues' ); // Load next page for current category const loadNextPage = useCallback(() => { if (selectedCategory === 'Dial Number' && hasMoreDialNumbers && !loadingDialNumbers) { - loadDialNumbers(dialNumbersPage + 1, searchQuery, false); + loadDialNumbers(dialNumbersPage + 1, searchQuery); } else if (selectedCategory === 'Entry Point' && hasMoreEntryPoints && !loadingEntryPoints) { - loadEntryPoints(entryPointsPage + 1, searchQuery, false); + loadEntryPoints(entryPointsPage + 1, searchQuery); + } else if (selectedCategory === 'Queues' && hasMoreQueues && !loadingQueues) { + loadQueues(queuesPage + 1, searchQuery); } }, [ selectedCategory, hasMoreDialNumbers, hasMoreEntryPoints, + hasMoreQueues, loadingDialNumbers, loadingEntryPoints, + loadingQueues, dialNumbersPage, entryPointsPage, + queuesPage, searchQuery, loadDialNumbers, loadEntryPoints, + loadQueues, ]); - // Debounced search function - const debouncedSearch = useCallback( - debounce((query: string, category: CategoryType) => { - if (category === 'Dial Number') { - setDialNumbersPage(0); // Reset pagination - setHasMoreDialNumbers(true); // Reset pagination state - loadDialNumbers(0, query, true); - } else if (category === 'Entry Point') { - setEntryPointsPage(0); // Reset pagination - setHasMoreEntryPoints(true); // Reset pagination state - loadEntryPoints(0, query, true); + // Create a stable debounced search function + const debouncedSearchRef = useRef>(); + + // Initialize debounced function once + if (!debouncedSearchRef.current) { + debouncedSearchRef.current = debounce((query: string, category: CategoryType) => { + // Only search if query is empty (to show all results) or has at least 2 characters + if (query.length === 0 || query.length >= 2) { + if (category === 'Dial Number') { + loadDialNumbers(0, query, true); + } else if (category === 'Entry Point') { + loadEntryPoints(0, query, true); + } else if (category === 'Queues') { + loadQueues(0, query, true); + } } - }, 300), - [loadDialNumbers, loadEntryPoints] - ); + }, 500); + } + + // Cleanup debounced function on unmount + useEffect(() => { + return () => { + // Clear any pending timeouts when component unmounts + if (debouncedSearchRef.current) { + // The debounce function should have a cancel method, but since our implementation doesn't, + // we'll just clear the ref + debouncedSearchRef.current = undefined; + } + }; + }, []); // Handle search input change const handleSearchChange = useCallback( (value: string) => { setSearchQuery(value); - - // For categories that support API search, use debounced search - if (selectedCategory === 'Dial Number' || selectedCategory === 'Entry Point') { - debouncedSearch(value, selectedCategory); - } - // For Agents and Queues, trigger fresh API calls if search functions are available - else if (selectedCategory === 'Agents' && getBuddyAgents) { - // Reset search query and let the filtered memoized result handle client-side filtering - // In future, this could be enhanced to use getBuddyAgents(value) for server-side search - } else if (selectedCategory === 'Queues' && getQueues) { - // Reset search query and let the filtered memoized result handle client-side filtering - // In future, this could be enhanced to use getQueues(value) for server-side search - } + debouncedSearchRef.current?.(value, selectedCategory); }, - [selectedCategory, debouncedSearch, getBuddyAgents, getQueues] + [selectedCategory] ); // Handle category change const handleCategoryChange = useCallback( (category: CategoryType) => { - console.log(`Category change attempted: ${category}`); // Console log for immediate debugging logger?.info(`CC-Components: Category changed to: ${category}`); setSelectedCategory(category); setSearchQuery(''); - // Reset pagination state for all categories - setDialNumbersPage(0); - setHasMoreDialNumbers(true); - setEntryPointsPage(0); - setHasMoreEntryPoints(true); - - // Clear existing data and let useEffect handle loading - if (category === 'Dial Number') { - setDialNumbers([]); // Clear existing data - } else if (category === 'Entry Point') { - setEntryPoints([]); // Clear existing data - } + // Reset all data sources + resetDialNumbers(); + resetEntryPoints(); + resetQueues(); }, - [logger] + [logger, resetDialNumbers, resetEntryPoints, resetQueues] ); - // Alternative click handlers for individual radio buttons - const handleAgentsClick = useCallback(() => { - console.log('Agents clicked directly'); - handleCategoryChange('Agents'); - }, [handleCategoryChange]); + // Click handlers for category buttons + const createCategoryClickHandler = (category: CategoryType) => () => handleCategoryChange(category); + const handleAgentsClick = createCategoryClickHandler('Agents'); + const handleQueuesClick = createCategoryClickHandler('Queues'); + const handleDialNumberClick = createCategoryClickHandler('Dial Number'); + const handleEntryPointClick = createCategoryClickHandler('Entry Point'); - const handleQueuesClick = useCallback(() => { - console.log('Queues clicked directly'); - handleCategoryChange('Queues'); - }, [handleCategoryChange]); + // Fetch buddy agents once on component mount + useEffect(() => { + const loadBuddyAgents = async () => { + if (!getBuddyAgents) return; - const handleDialNumberClick = useCallback(() => { - console.log('Dial Number clicked directly'); - handleCategoryChange('Dial Number'); - }, [handleCategoryChange]); + setLoadingBuddyAgents(true); + try { + logger?.info('CC-Components: Loading buddy agents'); + const agents = await getBuddyAgents(); + setBuddyAgents(agents || []); + logger?.info(`CC-Components: Loaded ${agents?.length || 0} buddy agents`); + } catch (error) { + logger?.error('CC-Components: Error loading buddy agents:', error); + setBuddyAgents([]); + } finally { + setLoadingBuddyAgents(false); + } + }; - const handleEntryPointClick = useCallback(() => { - console.log('Entry Point clicked directly'); - handleCategoryChange('Entry Point'); - }, [handleCategoryChange]); + loadBuddyAgents(); + }, [getBuddyAgents, logger]); // Intersection Observer for infinite scroll useEffect(() => { @@ -288,52 +242,26 @@ const ConsultTransferPopoverComponent: React.FC { - if (selectedCategory === 'Dial Number' && !loadingDialNumbers) { - // Always load if we don't have data OR if we just switched to this category - if (dialNumbers.length === 0) { - logger?.info('CC-Components: Loading dial numbers for first time or after category switch'); - loadDialNumbers(0, '', true); - } - } else if (selectedCategory === 'Entry Point' && !loadingEntryPoints) { - // Always load if we don't have data OR if we just switched to this category - if (entryPoints.length === 0) { - logger?.info('CC-Components: Loading entry points for first time or after category switch'); - loadEntryPoints(0, '', true); - } + if (selectedCategory === 'Dial Number' && dialNumbers.length === 0) { + loadDialNumbers(0, '', true); + } else if (selectedCategory === 'Entry Point' && entryPoints.length === 0) { + loadEntryPoints(0, '', true); + } else if (selectedCategory === 'Queues' && queues.length === 0) { + loadQueues(0, '', true); } - }, [ - selectedCategory, - dialNumbers.length, - entryPoints.length, - loadDialNumbers, - loadEntryPoints, - loadingDialNumbers, - loadingEntryPoints, - logger, - ]); + }, [selectedCategory]); // Filter agents based on search (client-side) const filteredAgents = useMemo(() => { - if (selectedCategory !== 'Agents' || !searchQuery) { - return buddyAgents; - } + if (!searchQuery) return buddyAgents; const query = searchQuery.toLowerCase(); return buddyAgents.filter((agent) => agent.agentName.toLowerCase().includes(query)); - }, [buddyAgents, searchQuery, selectedCategory]); - - // Filter queues based on search (client-side for now) - const filteredQueues = useMemo(() => { - if (selectedCategory !== 'Queues' || !searchQuery || !queues) { - return queues || []; - } - const query = searchQuery.toLowerCase(); - return queues.filter((queue) => queue.name.toLowerCase().includes(query)); - }, [queues, searchQuery, selectedCategory]); + }, [buddyAgents, searchQuery]); const noAgents = isAgentsEmpty(filteredAgents); - const noQueues = isQueuesEmpty(filteredQueues); + const noQueues = queues.length === 0; const noDialNumbers = dialNumbers.length === 0; const noEntryPoints = entryPoints.length === 0; @@ -374,16 +302,6 @@ const ConsultTransferPopoverComponent: React.FC { - logger?.info(`CC-Components: Debug - selectedCategory: ${selectedCategory}`); - logger?.info(`CC-Components: Debug - buddyAgents: ${JSON.stringify(buddyAgents?.slice(0, 2) || [])}`); - logger?.info(`CC-Components: Debug - queues: ${JSON.stringify(queues?.slice(0, 2) || [])}`); - logger?.info(`CC-Components: Debug - dialNumbers: ${JSON.stringify(dialNumbers.slice(0, 2))}`); - logger?.info(`CC-Components: Debug - entryPoints: ${JSON.stringify(entryPoints.slice(0, 2))}`); - logger?.info(`CC-Components: Debug - hasAnyData: ${hasAnyData}`); - }, [selectedCategory, buddyAgents, queues, dialNumbers, entryPoints, hasAnyData, logger]); - return (
@@ -444,7 +362,15 @@ const ConsultTransferPopoverComponent: React.FC} {/* Render content based on selected category */} - {selectedCategory === 'Agents' && noAgents && ( + {selectedCategory === 'Agents' && loadingBuddyAgents && ( +
+ + Loading agents... + +
+ )} + + {selectedCategory === 'Agents' && !loadingBuddyAgents && noAgents && ( )} @@ -462,6 +388,7 @@ const ConsultTransferPopoverComponent: React.FC queue.id, - (queue) => queue.name, - (id, name) => { - handleQueueSelection(id, name, onQueueSelect, logger); - } - )} - {selectedCategory === 'Dial Number' && !noDialNumbers && (
{renderList( @@ -558,6 +474,42 @@ const ConsultTransferPopoverComponent: React.FC )} + + {selectedCategory === 'Queues' && !noQueues && ( +
+ {renderList( + queues, + (queue) => queue.id, + (queue) => queue.name, + (id, name) => { + handleQueueSelection(id, name, onQueueSelect, logger); + } + )} + {/* Infinite scroll trigger */} + {hasMoreQueues && ( +
+ {loadingQueues ? ( + + Loading more queues... + + ) : ( + + Scroll to load more + + )} +
+ )} +
+ )}
); }; diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/usePaginatedData.ts b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/usePaginatedData.ts new file mode 100644 index 000000000..9e655ddff --- /dev/null +++ b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/usePaginatedData.ts @@ -0,0 +1,97 @@ +import {useState, useCallback} from 'react'; + +type Logger = { + info: (message: string) => void; + error: (message: string, error?: unknown) => void; +}; + +type FetchFunction = (params: { + page: number; + pageSize: number; + search?: string; +}) => Promise<{data: T[]; meta?: {page?: number; totalPages?: number}}>; + +type TransformFunction = (item: T, page: number, index: number) => U; + +export const usePaginatedData = ( + fetchFunction: FetchFunction | undefined, + transformFunction: TransformFunction, + logger: Logger | undefined, + categoryName: string +) => { + const [data, setData] = useState([]); + const [page, setPage] = useState(0); + const [hasMore, setHasMore] = useState(true); + const [loading, setLoading] = useState(false); + + const loadData = useCallback( + async (currentPage = 0, search = '', reset = false) => { + if (!fetchFunction) { + setData([]); + setHasMore(false); + return; + } + + setLoading(true); + try { + const apiParams: {page: number; pageSize: number; search?: string} = { + page: currentPage, + pageSize: 25, + }; + + if (search && search.trim()) { + apiParams.search = search; + } + + logger?.info(`CC-Components: Loading ${categoryName} - page: ${currentPage}, search: "${search}"`); + const response = await fetchFunction(apiParams); + + if (!response || !response.data) { + logger?.error(`CC-Components: Invalid response from fetch function for ${categoryName}`); + if (reset || currentPage === 0) { + setData([]); + } + setHasMore(false); + return; + } + + logger?.info(`CC-Components: Loaded ${response.data.length} ${categoryName} for page ${currentPage}`); + + const transformedEntries = response.data.map((entry, index) => transformFunction(entry, currentPage, index)); + + if (reset || currentPage === 0) { + setData(transformedEntries); + } else { + setData((prev) => [...prev, ...transformedEntries]); + } + + const newPage = response.meta?.page ?? currentPage; + const totalPages = response.meta?.totalPages ?? 1; + + setPage(newPage); + setHasMore(newPage < totalPages - 1); + + logger?.info( + `CC-Components: ${categoryName} pagination state - current: ${newPage}, total: ${totalPages}, hasMore: ${newPage < totalPages - 1}` + ); + } catch (error) { + logger?.error(`CC-Components: Error loading ${categoryName}:`, error); + if (reset || currentPage === 0) { + setData([]); + } + setHasMore(false); + } finally { + setLoading(false); + } + }, + [fetchFunction, transformFunction, logger, categoryName] + ); + + const reset = useCallback(() => { + setData([]); + setPage(0); + setHasMore(true); + }, []); + + return {data, page, hasMore, loading, loadData, reset}; +}; diff --git a/packages/contact-center/cc-components/src/components/task/task.types.ts b/packages/contact-center/cc-components/src/components/task/task.types.ts index 755aa5d31..14c184571 100644 --- a/packages/contact-center/cc-components/src/components/task/task.types.ts +++ b/packages/contact-center/cc-components/src/components/task/task.types.ts @@ -8,6 +8,8 @@ import { ContactServiceQueue, } from '@webex/cc-store'; +export type {BuddyDetails}; + type Enum> = T[keyof T]; /** @@ -498,8 +500,6 @@ export interface ConsultTransferListComponentProps { export interface ConsultTransferPopoverComponentProps { heading: string; buttonIcon: string; - buddyAgents: BuddyDetails[]; - queues?: ContactServiceQueue[]; onAgentSelect?: (agentId: string, agentName: string) => void; onQueueSelect?: (queueId: string, queueName: string) => void; onDialNumberSelect?: (id: string, name: string, number: string) => void; @@ -507,9 +507,9 @@ export interface ConsultTransferPopoverComponentProps { allowConsultToQueue: boolean; logger: ILogger; getAddressBookEntries: (params?: AddressBookEntrySearchParams) => Promise; - getEntryPoints: () => Promise; + getEntryPoints: (params?: EntryPointSearchParams) => Promise; getBuddyAgents?: (searchTerm?: string) => Promise; - getQueues?: (searchTerm?: string) => Promise; + getQueues?: (params?: QueueSearchParams) => Promise; } /** @@ -732,3 +732,134 @@ export interface AddressBookEntriesResponse { links?: Record; }; } + +/** + * Entry Point Search Parameters + */ +export interface EntryPointSearchParams { + /** Search keyword for name and number fields */ + search?: string; + + /** Page number (starts from 0) */ + page?: number; + + /** Number of items per page (default: 100) */ + pageSize?: number; + + /** Filter criteria */ + filter?: string; + + /** Additional attributes to be returned */ + attributes?: string; +} + +export interface EntryPointEntry { + /** Unique identifier for the entry */ + id: string; + + /** Name of the entry point */ + name: string; + + /** Phone number for the entry point */ + number: string; + + /** Organization ID this entry belongs to */ + organizationId?: string; + + /** Additional entry point specific fields */ + displayName?: string; + phoneNumber?: string; + extension?: string; +} + +export interface EntryPointEntriesResponse { + /** Array of entry point entries */ + data: EntryPointEntry[]; + + /** Pagination metadata */ + meta: { + /** Organization ID */ + orgid?: string; + + /** Current page number */ + page?: number; + + /** Page size for current data set */ + pageSize?: number; + + /** Number of pages */ + totalPages?: number; + + /** Total number of items */ + totalRecords?: number; + + /** Map of pagination links */ + links?: Record; + }; +} + +/** + * Queue Search Parameters + */ +export interface QueueSearchParams { + /** Search keyword for name and other fields */ + search?: string; + + /** Page number (starts from 0) */ + page?: number; + + /** Number of items per page (default: 100) */ + pageSize?: number; + + /** Filter criteria */ + filter?: string; + + /** Additional attributes to be returned */ + attributes?: string; +} + +export interface QueueEntry { + /** Unique identifier for the queue */ + id: string; + + /** Name of the queue */ + name: string; + + /** Description of the queue */ + description?: string; + + /** Organization ID this queue belongs to */ + organizationId?: string; + + /** Queue type */ + type?: string; + + /** Queue status */ + status?: string; +} + +export interface QueueEntriesResponse { + /** Array of queue entries */ + data: QueueEntry[]; + + /** Pagination metadata */ + meta: { + /** Organization ID */ + orgid?: string; + + /** Current page number */ + page?: number; + + /** Page size for current data set */ + pageSize?: number; + + /** Number of pages */ + totalPages?: number; + + /** Total number of items */ + totalRecords?: number; + + /** Map of pagination links */ + links?: Record; + }; +} diff --git a/packages/contact-center/store/src/storeEventsWrapper.ts b/packages/contact-center/store/src/storeEventsWrapper.ts index 19d3a03f8..e31bdd288 100644 --- a/packages/contact-center/store/src/storeEventsWrapper.ts +++ b/packages/contact-center/store/src/storeEventsWrapper.ts @@ -654,35 +654,6 @@ class StoreWrapper implements IStoreWrapper { this.handleTaskRemove(task.data.interactionId); }; - getBuddyAgents = async ( - mediaType: string = this.currentTask.data.interaction.mediaType - ): Promise> => { - try { - const response = await this.store.cc.getBuddyAgents({ - //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 - mediaType: mediaType ?? 'telephony', - state: 'Available', - }); - return 'data' in response ? response.data.agentList : []; - } catch (error) { - return Promise.reject(error); - } - }; - - getQueues = async ( - mediaType: string = this.currentTask.data.interaction.mediaType ?? 'TELEPHONY' - ): Promise> => { - try { - const upperMediaType = mediaType.toUpperCase(); - let queueList = await this.store.cc.getQueues(); - queueList = queueList.filter((queue) => queue.channelType === upperMediaType); - return queueList; - } catch (error) { - console.error('Error fetching queues:', error); - return Promise.reject(error); - } - }; - cleanUpStore = () => { this.store.logger.info('CC-Widgets: cleanUpStore(): resetting store on logout', { module: 'storeEventsWrapper.ts', diff --git a/packages/contact-center/task/src/helper.ts b/packages/contact-center/task/src/helper.ts index 5b0743640..1476bbf44 100644 --- a/packages/contact-center/task/src/helper.ts +++ b/packages/contact-center/task/src/helper.ts @@ -394,33 +394,6 @@ export const useCallControl = (props: useCallControlProps) => { } }, [currentTask, extractConsultingAgent, consultInitiated]); - const loadBuddyAgents = useCallback(async () => { - try { - const agents = await store.getBuddyAgents(); - logger.info(`Loaded ${agents.length} buddy agents`, {module: 'helper.ts', method: 'loadBuddyAgents'}); - setBuddyAgents(agents); - } catch (error) { - logger?.error(`CC-Widgets: Task: Error loading buddy agents - ${error.message || error}`, { - module: 'useCallControl', - method: 'loadBuddyAgents', - }); - setBuddyAgents([]); - } - }, [logger]); - - const loadQueues = useCallback(async () => { - try { - const queues = await store.getQueues(); - setQueues(queues); - } catch (error) { - logger?.error(`CC-Widgets: Task: Error loading queues - ${error.message || error}`, { - module: 'useCallControl', - method: 'loadQueues', - }); - setQueues([]); - } - }, [logger]); - const holdCallback = () => { try { setIsHeld(true); diff --git a/widgets-samples/cc/samples-cc-react-app/src/App.tsx b/widgets-samples/cc/samples-cc-react-app/src/App.tsx index c0bb06dc0..1ea04d9e1 100644 --- a/widgets-samples/cc/samples-cc-react-app/src/App.tsx +++ b/widgets-samples/cc/samples-cc-react-app/src/App.tsx @@ -858,8 +858,8 @@ function App() { getAddressBookEntries={async (params) => { return await store.cc.addressBook.getEntries(params); }} - getEntryPoints={async () => { - return await store.cc.entryPoints.getEntryPoints(); + getEntryPoints={async (params) => { + return await store.cc.entryPoints.getEntryPoints(params); }} getBuddyAgents={async (searchTerm?: string): Promise => { try { @@ -879,8 +879,8 @@ function App() { return []; } }} - getQueues={async (searchTerm?: string) => { - return await store.cc.getQueues(searchTerm); + getQueues={async (params) => { + return await store.cc.queue.getQueues(params); }} /> From 0374dbd5565b65d6d97768e8bdb7a5aae5a25973 Mon Sep 17 00:00:00 2001 From: arungane Date: Wed, 17 Sep 2025 21:45:47 -0400 Subject: [PATCH 3/4] fix: get mediatype by task --- widgets-samples/cc/samples-cc-react-app/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widgets-samples/cc/samples-cc-react-app/src/App.tsx b/widgets-samples/cc/samples-cc-react-app/src/App.tsx index 1ea04d9e1..a85610855 100644 --- a/widgets-samples/cc/samples-cc-react-app/src/App.tsx +++ b/widgets-samples/cc/samples-cc-react-app/src/App.tsx @@ -865,7 +865,7 @@ function App() { try { // Create the BuddyAgents data object with required mediaType const buddyAgentsData: BuddyAgents = { - mediaType: 'telephony', // Required field + mediaType: store.currentTask?.data?.interaction?.mediaType || 'telephony', // Use dynamic mediaType from current task ...(searchTerm && {state: 'Available'}), // Use state filter when search term is provided }; const response = await store.cc.getBuddyAgents(buddyAgentsData); From 76acf95fca7533a105ab4ad8c3f1cf57d82cc9db Mon Sep 17 00:00:00 2001 From: arungane Date: Thu, 18 Sep 2025 12:21:02 -0400 Subject: [PATCH 4/4] fix: agent and search functionality --- .../consult-transfer-popover.tsx | 63 +++++++++++++++---- .../cc/samples-cc-react-app/src/App.tsx | 7 +-- 2 files changed, 52 insertions(+), 18 deletions(-) diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx index e8cafd24c..c58c3eece 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx @@ -171,7 +171,11 @@ const ConsultTransferPopoverComponent: React.FC { setSearchQuery(value); - debouncedSearchRef.current?.(value, selectedCategory); + // Only trigger server search for non-agent categories + // Agents use local search via filteredAgents useMemo + if (selectedCategory !== 'Agents') { + debouncedSearchRef.current?.(value, selectedCategory); + } }, [selectedCategory] ); @@ -255,10 +259,32 @@ const ConsultTransferPopoverComponent: React.FC { - if (!searchQuery) return buddyAgents; - const query = searchQuery.toLowerCase(); - return buddyAgents.filter((agent) => agent.agentName.toLowerCase().includes(query)); - }, [buddyAgents, searchQuery]); + logger?.info( + `CC-Components: filteredAgents useMemo triggered - searchQuery: "${searchQuery}", selectedCategory: "${selectedCategory}", buddyAgents.length: ${buddyAgents.length}` + ); + + if (!searchQuery || searchQuery.trim() === '') { + logger?.info(`CC-Components: No search query, returning all ${buddyAgents.length} agents`); + return buddyAgents; + } + + const query = searchQuery.toLowerCase().trim(); + logger?.info(`CC-Components: Filtering agents with query: "${query}", total agents: ${buddyAgents.length}`); + + const filtered = buddyAgents.filter((agent) => { + const agentName = (agent.agentName || '').toLowerCase(); + const matches = agentName.includes(query); + + if (matches) { + logger?.info(`CC-Components: Agent matched - ID: ${agent.agentId}, Name: ${agent.agentName}`); + } + + return matches; + }); + + logger?.info(`CC-Components: Filtered agents count: ${filtered.length}`); + return filtered; + }, [buddyAgents, searchQuery, logger]); const noAgents = isAgentsEmpty(filteredAgents); const noQueues = queues.length === 0; @@ -389,15 +415,26 @@ const ConsultTransferPopoverComponent: React.FC agent.agentId, - (agent) => agent.agentName, - (id, name) => { - handleAgentSelection(id, name, onAgentSelect, logger); + (() => { + logger?.info( + `CC-Components: Rendering agents - noAgents: ${noAgents}, filteredAgents.length: ${filteredAgents.length}, selectedCategory: ${selectedCategory}` + ); + + if (noAgents) { + logger?.info(`CC-Components: No agents to render - showing empty state`); + return null; // This will be handled by the empty state above } - )} + + logger?.info(`CC-Components: Rendering ${filteredAgents.length} agents`); + return renderList( + filteredAgents, + (agent) => agent.agentId, + (agent) => agent.agentName, + (id, name) => { + handleAgentSelection(id, name, onAgentSelect, logger); + } + ); + })()} {selectedCategory === 'Dial Number' && !noDialNumbers && (
diff --git a/widgets-samples/cc/samples-cc-react-app/src/App.tsx b/widgets-samples/cc/samples-cc-react-app/src/App.tsx index a85610855..da15ce67c 100644 --- a/widgets-samples/cc/samples-cc-react-app/src/App.tsx +++ b/widgets-samples/cc/samples-cc-react-app/src/App.tsx @@ -869,11 +869,8 @@ function App() { ...(searchTerm && {state: 'Available'}), // Use state filter when search term is provided }; const response = await store.cc.getBuddyAgents(buddyAgentsData); - // Return the buddy agents array from the response - if (response && typeof response === 'object' && 'agentList' in response) { - return response.agentList || []; - } - return []; + + return 'data' in response ? response.data.agentList : []; } catch (error) { console.error('Error fetching buddy agents:', error); return [];