@@ -25,8 +25,8 @@ import store, {
2525 isInteractionOnHold ,
2626 MEDIA_TYPE_TELEPHONY_LOWER ,
2727} from '@webex/cc-store' ;
28- import { TIMER_LABEL_CONSULTING } from './Utils/constants' ;
29- import { calculateStateTimerData , calculateConsultTimerData } from './Utils/timer-utils' ;
28+ import { TIMER_LABEL_CONSULTING , TIMER_LABEL_CONSULT_REQUESTED , TIMER_LABEL_CONSULT_ON_HOLD , TIMER_LABEL_WRAP_UP } from './Utils/constants' ;
29+ import { calculateStateTimerData , calculateConsultTimerData , findLatestConsultMedia } from './Utils/timer-utils' ;
3030import { useHoldTimer } from './Utils/useHoldTimer' ;
3131import { OutdialAniEntriesResponse } from '@webex/contact-center/dist/types/services/config/types' ;
3232
@@ -279,7 +279,18 @@ export const useIncomingTask = (props: UseTaskProps) => {
279279} ;
280280
281281export const useCallControl = ( props : useCallControlProps ) => {
282- const { currentTask, onHoldResume, onEnd, onWrapUp, onRecordingToggle, onToggleMute, logger, isMuted, agentId, conferenceEnabled = true } = props ;
282+ const {
283+ currentTask,
284+ onHoldResume,
285+ onEnd,
286+ onWrapUp,
287+ onRecordingToggle,
288+ onToggleMute,
289+ logger,
290+ isMuted,
291+ agentId,
292+ conferenceEnabled = true ,
293+ } = props ;
283294 const [ isRecording , setIsRecording ] = useState ( true ) ;
284295 const [ controls , setControls ] = useState < TaskUIControls > ( currentTask ?. uiControls ?? getDefaultUIControls ( ) ) ;
285296 const [ isHeld , setIsHeld ] = useState < boolean > ( ( ) => ( currentTask ? isInteractionOnHold ( currentTask ) : false ) ) ;
@@ -296,6 +307,7 @@ export const useCallControl = (props: useCallControlProps) => {
296307 // Consult timer labels and timestamps
297308 const [ consultTimerLabel , setConsultTimerLabel ] = useState < string > ( TIMER_LABEL_CONSULTING ) ;
298309 const [ consultTimerTimestamp , setConsultTimerTimestamp ] = useState < number > ( 0 ) ;
310+ const prevIsConsultingRef = useRef ( false ) ;
299311 const [ lastTargetType , setLastTargetType ] = useState < TargetType > ( TARGET_TYPE . AGENT ) ;
300312 const [ conferenceParticipants , setConferenceParticipants ] = useState < Participant [ ] > ( [ ] ) ;
301313 const lastWrapupAuxCodeIdRef = useRef < string | null > ( null ) ;
@@ -922,10 +934,6 @@ export const useCallControl = (props: useCallControlProps) => {
922934 }
923935
924936 try {
925- // When consulting (even from within a conference), use regular transfer.
926- // transferConference() is only for transferring the entire conference ownership,
927- // not for transferring a consult to join the conference.
928- // Check state machine: CONSULTING state means we should use transfer(), not transferConference()
929937 const currentState = currentTask . state ?. value ;
930938 const isCurrentlyConsulting = currentState === 'CONSULTING' ;
931939
@@ -936,13 +944,40 @@ export const useCallControl = (props: useCallControlProps) => {
936944 } ) ;
937945 await currentTask . transferConference ( ) ;
938946 } else {
939- logger . info ( 'Consult transfer initiated' , { module : 'useCallControl' , method : 'consultTransfer' } ) ;
940- await currentTask . transfer (
941- store . lastConsultDestination ?? {
942- to : currentTask . data . destAgentId ,
943- destinationType : 'agent' as DestinationType ,
947+ let destination = store . lastConsultDestination ;
948+
949+ if ( ! destination ?. to ) {
950+ // After page refresh, lastConsultDestination is lost (in-memory only).
951+ // Recover the transfer target from the consult media's participants.
952+ const myAgentId = store . cc . agentConfig ?. agentId ;
953+ const { interaction} = currentTask . data ;
954+ const consultMediaId = findMediaResourceId ( currentTask , 'consult' ) ;
955+ const consultMedia = consultMediaId ? interaction ?. media ?. [ consultMediaId ] : null ;
956+
957+ let consultedAgentId : string | null = null ;
958+ if ( consultMedia ?. participants ) {
959+ consultedAgentId = consultMedia . participants . find ( ( pid : string ) => {
960+ const p = interaction ?. participants ?. [ pid ] ;
961+ return p && p . id !== myAgentId && p . pType === 'Agent' ;
962+ } ) ?? null ;
944963 }
945- ) ;
964+
965+ if ( consultedAgentId ) {
966+ destination = { to : consultedAgentId , destinationType : 'agent' as DestinationType } ;
967+ logger . info ( `Recovered consult destination from interaction data: ${ consultedAgentId } ` , {
968+ module : 'useCallControl' ,
969+ method : 'consultTransfer' ,
970+ } ) ;
971+ }
972+ }
973+
974+ if ( ! destination ?. to ) {
975+ logError ( 'Cannot transfer: consult destination not found' , 'consultTransfer' ) ;
976+ return ;
977+ }
978+
979+ logger . info ( 'Consult transfer initiated' , { module : 'useCallControl' , method : 'consultTransfer' } ) ;
980+ await currentTask . transfer ( destination ) ;
946981 }
947982 } catch ( error ) {
948983 logError ( `Error transferring consult call: ${ error } ` , 'consultTransfer' ) ;
@@ -966,17 +1001,39 @@ export const useCallControl = (props: useCallControlProps) => {
9661001 currentTask . cancelAutoWrapupTimer ( ) ;
9671002 } ;
9681003
969- // Add useEffect for auto wrap-up timer
1004+ // Derive stable primitives from MobX-observed task data so that effects
1005+ // re-fire when the backend pushes fresh interaction/participant state —
1006+ // not only when controls change. `currentTask` is a MobX proxy whose
1007+ // reference never changes, so effects would otherwise miss data-only
1008+ // updates.
1009+ const _interaction = currentTask ?. data ?. interaction ;
1010+ const _participant = _interaction ?. participants ?. [ agentId ] ;
1011+
1012+ // Consult-timer primitives
1013+ const _consultMedia = findLatestConsultMedia ( _interaction ) ;
1014+ const consultMediaIsHold = ! ! _consultMedia ?. isHold ;
1015+ const consultMediaId = _consultMedia ?. mediaResourceId ?? '' ;
1016+ const participantConsultState = _participant ?. consultState ?? null ;
1017+
1018+ // State-timer (wrap-up / post-call) primitives
1019+ const participantIsWrapUp = ! ! _participant ?. isWrapUp ;
1020+ const participantWrapUpTimestamp = _participant ?. wrapUpTimestamp ?? 0 ;
1021+ const participantLastUpdated = _participant ?. lastUpdated ?? 0 ;
1022+ const participantCurrentState = _participant ?. currentState ?? null ;
1023+ const interactionState = _interaction ?. state ?? null ;
1024+
1025+ // Auto wrap-up timer.
1026+ // `currentTask.autoWrapup` must remain a useEffect dependency so that when
1027+ // the SDK sets it (after the initial wrapup render), React detects the
1028+ // change on the next re-render and re-fires the effect.
9701029 useEffect ( ( ) => {
9711030 let timerId : ReturnType < typeof setInterval > ;
9721031
9731032 if ( currentTask ?. autoWrapup && controls ?. main ?. wrapup ) {
9741033 try {
975- // Initialize time left from the autoWrapup object
9761034 const initialTimeLeft = currentTask . autoWrapup . getTimeLeftSeconds ( ) ;
9771035 setsecondsUntilAutoWrapup ( initialTimeLeft ) ;
9781036
979- // Update timer every second
9801037 timerId = setInterval ( ( ) => {
9811038 setsecondsUntilAutoWrapup ( ( prevTime ) => {
9821039 if ( prevTime && prevTime > 0 ) {
@@ -994,27 +1051,63 @@ export const useCallControl = (props: useCallControlProps) => {
9941051 }
9951052 }
9961053
997- // Clear the interval when component unmounts or when auto wrap-up is no longer active
9981054 return ( ) => {
9991055 if ( timerId ) {
10001056 clearInterval ( timerId ) ;
10011057 }
10021058 } ;
10031059 } , [ currentTask ?. autoWrapup , controls ?. main ?. wrapup ] ) ;
10041060
1005- // Calculate state timer label and timestamp using utils
1061+ // Calculate state timer label and timestamp (Wrap Up / Post Call).
1062+ // When the SDK sets wrapup controls visible (ContactEnded event), the
1063+ // participant data may not yet contain the wrapup timestamp (it arrives
1064+ // in the subsequent AgentWrapup event). Bridge this gap by showing the
1065+ // "Wrap Up" label immediately with Date.now() as a close approximation;
1066+ // the timer auto-corrects when the real timestamp arrives.
10061067 useEffect ( ( ) => {
10071068 const stateTimerData = calculateStateTimerData ( currentTask , controls , agentId ) ;
1008- setStateTimerLabel ( stateTimerData . label ) ;
1009- setStateTimerTimestamp ( stateTimerData . timestamp ) ;
1010- } , [ currentTask , controls , agentId ] ) ;
10111069
1012- // Calculate consult timer label and timestamp using utils
1070+ if ( stateTimerData . label && stateTimerData . timestamp ) {
1071+ setStateTimerLabel ( stateTimerData . label ) ;
1072+ setStateTimerTimestamp ( stateTimerData . timestamp ) ;
1073+ } else if ( controls ?. main ?. wrapup ?. isVisible ) {
1074+ setStateTimerLabel ( TIMER_LABEL_WRAP_UP ) ;
1075+ setStateTimerTimestamp ( ( prev ) => prev || Date . now ( ) ) ;
1076+ } else {
1077+ setStateTimerLabel ( stateTimerData . label ) ;
1078+ setStateTimerTimestamp ( stateTimerData . timestamp ) ;
1079+ }
1080+ } , [
1081+ currentTask , controls , agentId ,
1082+ participantIsWrapUp , participantWrapUpTimestamp , participantLastUpdated ,
1083+ participantCurrentState , interactionState ,
1084+ ] ) ;
1085+
1086+ // Calculate consult timer label and timestamp.
1087+ // The calculation relies on consult media's isHold + holdTimestamp as the
1088+ // sole source of truth for "Consult on Hold" (same as the next branch).
1089+ //
1090+ // On hidden→visible transition (new consult starts), stale data from the
1091+ // previous flow may produce "Consult on Hold". Override to safe defaults.
1092+ // We never early-return — the calculation always runs — so that when data
1093+ // is already fresh (e.g., Agent 1 accepts a consult and data says
1094+ // "Consulting"), the correct label is applied immediately.
10131095 useEffect ( ( ) => {
1096+ const isConsulting = controls ?. consult ?. endConsult ?. isVisible || controls ?. main ?. endConsult ?. isVisible ;
1097+ const wasConsulting = prevIsConsultingRef . current ;
1098+ prevIsConsultingRef . current = ! ! isConsulting ;
1099+
10141100 const consultTimerData = calculateConsultTimerData ( currentTask , controls , agentId ) ;
1015- setConsultTimerLabel ( consultTimerData . label ) ;
1016- setConsultTimerTimestamp ( consultTimerData . timestamp ) ;
1017- } , [ currentTask , controls , agentId ] ) ;
1101+ const justBecameConsulting = isConsulting && ! wasConsulting ;
1102+
1103+ if ( justBecameConsulting && consultTimerData . label === TIMER_LABEL_CONSULT_ON_HOLD ) {
1104+ setConsultTimerLabel ( TIMER_LABEL_CONSULT_REQUESTED ) ;
1105+ setConsultTimerTimestamp ( 0 ) ;
1106+ } else {
1107+ setConsultTimerLabel ( consultTimerData . label ) ;
1108+ setConsultTimerTimestamp ( consultTimerData . timestamp ) ;
1109+ }
1110+ } , [ currentTask , controls , agentId , consultMediaIsHold , consultMediaId , participantConsultState ] ) ;
10181111
10191112 return {
10201113 currentTask,
0 commit comments