diff --git a/src/components/layout/header.tsx b/src/components/layout/header.tsx index 416ac350..1dfbd8e2 100644 --- a/src/components/layout/header.tsx +++ b/src/components/layout/header.tsx @@ -3,7 +3,6 @@ import { useState, useEffect, useCallback, useRef } from 'react' import { useRouter } from 'next/navigation' import { - Circle, LogOut, User, ChevronDown, @@ -33,12 +32,12 @@ import { AgentDiagnostic } from '@/components/phone/agent-diagnostic' const MANUAL_STATUSES: AgentStatus[] = ['available', 'busy', 'away', 'offline'] const statusConfig: Record = { - available: { label: 'Available', color: 'text-success', dot: 'bg-success' }, - busy: { label: 'Busy', color: 'text-destructive', dot: 'bg-destructive' }, - away: { label: 'Away', color: 'text-warning', dot: 'bg-warning' }, - on_call: { label: 'On Call', color: 'text-primary', dot: 'bg-primary' }, - wrap_up: { label: 'Wrap Up', color: 'text-warning', dot: 'bg-amber-500' }, - offline: { label: 'Offline', color: 'text-muted-foreground', dot: 'bg-muted-foreground' }, + available: { label: 'Available', color: 'text-success', dot: 'bg-success' }, + busy: { label: 'Busy', color: 'text-destructive', dot: 'bg-destructive' }, + away: { label: 'Away', color: 'text-warning', dot: 'bg-warning' }, + on_call: { label: 'On Call', color: 'text-primary', dot: 'bg-primary' }, + wrap_up: { label: 'Wrap Up', color: 'text-warning', dot: 'bg-amber-500' }, + offline: { label: 'Offline', color: 'text-muted-foreground', dot: 'bg-muted-foreground' }, } const KNOWN_STATUSES: AgentStatus[] = ['available', 'busy', 'away', 'offline', 'on_call', 'wrap_up'] @@ -76,38 +75,52 @@ export function Header() { return () => window.removeEventListener('agent-status-optimistic', handleOptimistic) }, []) - const handleStatusChange = useCallback(async (newStatus: AgentStatus): Promise => { - if (newStatus === 'available' && !isRegistered) { - showPresenceToast('Cannot go available — phone not connected. Please refresh.', 'error') - return false - } + const handleStatusChange = useCallback( + async (newStatus: AgentStatus): Promise => { + if (newStatus === 'available' && !isRegistered) { + showPresenceToast('Cannot go available — phone not connected. Please refresh.', 'error') + return false + } - const previousStatus = status - setStatus(newStatus) // optimistic UI - window.dispatchEvent( - new CustomEvent('agent-status-optimistic', { - detail: { from: previousStatus, to: newStatus }, - }) - ) + const previousStatus = status + setStatus(newStatus) // optimistic UI + window.dispatchEvent( + new CustomEvent('agent-status-optimistic', { + detail: { from: previousStatus, to: newStatus }, + }) + ) - try { - // Use the presence API so both agent_presence AND users.agent_status - // are updated atomically — no more competing Supabase direct writes. - const res = await fetch('/api/agents/presence', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ status: newStatus }), - }) - const payload = (await res.json().catch(() => null)) as - | { noop?: boolean; reason?: string } - | null + try { + // Use the presence API so both agent_presence AND users.agent_status + // are updated atomically — no more competing Supabase direct writes. + const res = await fetch('/api/agents/presence', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status: newStatus }), + }) + const payload = (await res.json().catch(() => null)) as + | { noop?: boolean; reason?: string } + | null + + if (!res.ok) throw new Error(`Presence update failed: ${res.status}`) - if (!res.ok) throw new Error(`Presence update failed: ${res.status}`) + if ( + payload?.noop === true && + (payload.reason === 'restricted_status' || payload.reason === 'concurrent_modification') + ) { + setStatus(previousStatus) + window.dispatchEvent( + new CustomEvent('agent-status-optimistic', { + detail: { from: newStatus, to: previousStatus }, + }) + ) + return false + } - if ( - payload?.noop === true && - (payload.reason === 'restricted_status' || payload.reason === 'concurrent_modification') - ) { + return true + } catch (err) { + console.error('[Header] Failed to persist agent status:', err) + // Revert optimistic update setStatus(previousStatus) window.dispatchEvent( new CustomEvent('agent-status-optimistic', { @@ -116,20 +129,9 @@ export function Header() { ) return false } - - return true - } catch (err) { - console.error('[Header] Failed to persist agent status:', err) - // Revert optimistic update - setStatus(previousStatus) - window.dispatchEvent( - new CustomEvent('agent-status-optimistic', { - detail: { from: newStatus, to: previousStatus }, - }) - ) - return false - } - }, [isRegistered, status]) + }, + [isRegistered, status] + ) useEffect(() => { if (isRegistered) { @@ -138,7 +140,9 @@ export function Header() { }, [isRegistered]) useEffect(() => { - const activeCall = currentCall?.state && ['ringing_inbound', 'ringing_outbound', 'answering', 'active', 'held'].includes(currentCall.state) + const activeCall = + currentCall?.state && + ['ringing_inbound', 'ringing_outbound', 'answering', 'active', 'held'].includes(currentCall.state) if (status === 'available' && hasInitialized && !isRegistered && !activeCall) { if (autoOfflineAttemptedRef.current) return autoOfflineAttemptedRef.current = true @@ -163,16 +167,26 @@ export function Header() { router.refresh() } - const initials = [user.first_name?.[0], user.last_name?.[0]] - .filter(Boolean) - .join('') - .toUpperCase() || user.email[0].toUpperCase() + const initials = + [user.first_name?.[0], user.last_name?.[0]].filter(Boolean).join('').toUpperCase() || + user.email[0].toUpperCase() - const displayName = - [user.first_name, user.last_name].filter(Boolean).join(' ') || user.email + const displayName = [user.first_name, user.last_name].filter(Boolean).join(' ') || user.email const currentConfig = statusConfig[status] ?? statusConfig['offline'] + const presenceBadgeState: 'online' | 'offline' | 'degraded' | 'on_call' = + status === 'on_call' || status === 'wrap_up' + ? 'on_call' + : status === 'available' && connectionState !== 'offline' + ? connectionState === 'degraded' + ? 'degraded' + : 'online' + : 'offline' + + const showOfflineBanner = status === 'offline' || connectionState === 'offline' + const showDegradedBanner = !showOfflineBanner && connectionState === 'degraded' + return ( <>
@@ -181,17 +195,22 @@ export function Header() { {user.role} - {connectionState === 'online' ? '🟢 ONLINE' : connectionState === 'degraded' ? '🟠 DEGRADED' : '🔴 OFFLINE'} + {presenceBadgeState === 'online' + ? '🟢 ONLINE' + : presenceBadgeState === 'degraded' + ? '🟠 DEGRADED' + : presenceBadgeState === 'on_call' + ? '🟡 ON CALL' + : '🔴 OFFLINE'} @@ -199,36 +218,40 @@ export function Header() { - {(status === 'on_call' || status === 'wrap_up') ? ( + {status === 'on_call' || status === 'wrap_up' ? (
- {status === 'wrap_up' && wrapUpSecondsRemaining > 0 ? `Wrap Up (${wrapUpSecondsRemaining}s)` : currentConfig.label} + + {status === 'wrap_up' && wrapUpSecondsRemaining > 0 + ? `Wrap Up (${wrapUpSecondsRemaining}s)` + : currentConfig.label} +
) : ( - - - - - - {MANUAL_STATUSES.map((key) => { - const config = statusConfig[key] - return ( - handleStatusChange(key)} - className="gap-2" - > - - {status === 'offline' && key === 'available' ? 'Go Available' : config.label} - - ) - })} - - + + + + + + {MANUAL_STATUSES.map((key) => { + const config = statusConfig[key] + return ( + handleStatusChange(key)} + className="gap-2" + > + + {status === 'offline' && key === 'available' ? 'Go Available' : config.label} + + ) + })} + + )} @@ -260,7 +283,10 @@ export function Header() { Settings - + Sign Out @@ -269,19 +295,21 @@ export function Header() {
- {connectionState !== 'online' && ( + {(showOfflineBanner || showDegradedBanner) && (
- {connectionState === 'degraded' - ? 'Connection is degraded. Calls should still route, but reconnecting is recommended.' - : 'You are currently offline. Calls will not be routed to you.'} + {status === 'offline' + ? 'You are currently offline. Calls will not be routed to you.' + : connectionState === 'offline' + ? 'Connection lost. Attempting to reconnect...' + : 'Connection is degraded. Calls should still route, but reconnecting is recommended.'}