diff --git a/src/components/call-desk/call-widget.tsx b/src/components/call-desk/call-widget.tsx new file mode 100644 index 0000000..b3f7c4b --- /dev/null +++ b/src/components/call-desk/call-widget.tsx @@ -0,0 +1,327 @@ +import { useState, useEffect } from 'react'; +import { + Phone01, + PhoneIncoming01, + PhoneOutgoing01, + PhoneHangUp, + PhoneX, + MicrophoneOff01, + Microphone01, + PauseCircle, + CheckCircle, + Save01, +} from '@untitledui/icons'; +import { Button } from '@/components/base/buttons/button'; +import { TextArea } from '@/components/base/textarea/textarea'; +import { useSip } from '@/providers/sip-provider'; +import { cx } from '@/utils/cx'; +import type { CallDisposition } from '@/types/entities'; + +const formatDuration = (seconds: number): string => { + const m = Math.floor(seconds / 60) + .toString() + .padStart(2, '0'); + const s = (seconds % 60).toString().padStart(2, '0'); + return `${m}:${s}`; +}; + +const statusDotColor: Record = { + registered: 'bg-success-500', + connecting: 'bg-warning-500', + disconnected: 'bg-quaternary', + error: 'bg-error-500', +}; + +const statusLabel: Record = { + registered: 'Ready', + connecting: 'Connecting...', + disconnected: 'Offline', + error: 'Error', +}; + +const dispositionOptions: Array<{ + value: CallDisposition; + label: string; + activeClass: string; + defaultClass: string; +}> = [ + { + value: 'APPOINTMENT_BOOKED', + label: 'Appt Booked', + activeClass: 'bg-success-solid text-white ring-transparent', + defaultClass: 'bg-success-primary text-success-primary border-success', + }, + { + value: 'FOLLOW_UP_SCHEDULED', + label: 'Follow-up', + activeClass: 'bg-brand-solid text-white ring-transparent', + defaultClass: 'bg-brand-primary text-brand-secondary border-brand', + }, + { + value: 'INFO_PROVIDED', + label: 'Info Given', + activeClass: 'bg-utility-blue-light-600 text-white ring-transparent', + defaultClass: 'bg-utility-blue-light-50 text-utility-blue-light-700 border-utility-blue-light-200', + }, + { + value: 'NO_ANSWER', + label: 'No Answer', + activeClass: 'bg-warning-solid text-white ring-transparent', + defaultClass: 'bg-warning-primary text-warning-primary border-warning', + }, + { + value: 'WRONG_NUMBER', + label: 'Wrong #', + activeClass: 'bg-secondary-solid text-white ring-transparent', + defaultClass: 'bg-secondary text-secondary border-secondary', + }, + { + value: 'CALLBACK_REQUESTED', + label: 'Not Interested', + activeClass: 'bg-error-solid text-white ring-transparent', + defaultClass: 'bg-error-primary text-error-primary border-error', + }, +]; + +export const CallWidget = () => { + const { + connectionStatus, + callState, + callerNumber, + isMuted, + isOnHold, + callDuration, + answer, + hangup, + toggleMute, + toggleHold, + } = useSip(); + + const [disposition, setDisposition] = useState(null); + const [notes, setNotes] = useState(''); + const [lastDuration, setLastDuration] = useState(0); + + // Capture duration right before call ends so we can display it in the ended state + useEffect(() => { + if (callState === 'active' && callDuration > 0) { + setLastDuration(callDuration); + } + }, [callState, callDuration]); + + // Reset disposition/notes when returning to idle + useEffect(() => { + if (callState === 'idle') { + setDisposition(null); + setNotes(''); + } + }, [callState]); + + const handleSaveAndClose = () => { + // TODO: Wire to BFF to create Call record + update Lead + hangup(); + setDisposition(null); + setNotes(''); + }; + + const dotColor = statusDotColor[connectionStatus] ?? 'bg-quaternary'; + const label = statusLabel[connectionStatus] ?? connectionStatus; + + // Idle: collapsed pill + if (callState === 'idle') { + return ( +
+ + {label} + Helix Phone +
+ ); + } + + // Ringing inbound + if (callState === 'ringing-in') { + return ( +
+
+
+
+ +
+
+ +
+ + Incoming Call + + {callerNumber ?? 'Unknown'} +
+ +
+ + +
+
+ ); + } + + // Ringing outbound + if (callState === 'ringing-out') { + return ( +
+ + +
+ + Calling... + + {callerNumber ?? 'Unknown'} +
+ + +
+ ); + } + + // Active call (full widget) + if (callState === 'active') { + return ( +
+ {/* Header */} +
+
+ + Active Call +
+ + {formatDuration(callDuration)} + +
+ + {/* Caller number */} + {callerNumber ?? 'Unknown'} + + {/* Call controls */} +
+ + + +
+ + {/* Divider */} +
+ + {/* Disposition */} +
+ Disposition +
+ {dispositionOptions.map((opt) => { + const isSelected = disposition === opt.value; + return ( + + ); + })} +
+ +