diff --git a/src/components/call-desk/call-widget.tsx b/src/components/call-desk/call-widget.tsx
index 29f7174..51df7b7 100644
--- a/src/components/call-desk/call-widget.tsx
+++ b/src/components/call-desk/call-widget.tsx
@@ -1,174 +1,41 @@
-import { useState, useEffect, useRef } from 'react';
-import {
- faPhone,
- faPhoneArrowDown,
- faPhoneArrowUp,
- faPhoneHangup,
- faPhoneXmark,
- faMicrophoneSlash,
- faMicrophone,
- faPause,
- faCircleCheck,
- faFloppyDisk,
- faCalendarPlus,
-} from '@fortawesome/pro-duotone-svg-icons';
+import { useEffect } from 'react';
+import { useNavigate, useLocation } from 'react-router';
+import { faPhone, faPhoneArrowDown, faPhoneXmark, faCircleCheck } from '@fortawesome/pro-duotone-svg-icons';
import { faIcon } from '@/lib/icon-wrapper';
const Phone01 = faIcon(faPhone);
const PhoneIncoming01 = faIcon(faPhoneArrowDown);
-const PhoneOutgoing01 = faIcon(faPhoneArrowUp);
-const PhoneHangUp = faIcon(faPhoneHangup);
const PhoneX = faIcon(faPhoneXmark);
-const MicrophoneOff01 = faIcon(faMicrophoneSlash);
-const Microphone01 = faIcon(faMicrophone);
-const PauseCircle = faIcon(faPause);
const CheckCircle = faIcon(faCircleCheck);
-const Save01 = faIcon(faFloppyDisk);
-const CalendarPlus02 = faIcon(faCalendarPlus);
import { Button } from '@/components/base/buttons/button';
-import { TextArea } from '@/components/base/textarea/textarea';
-import { AppointmentForm } from '@/components/call-desk/appointment-form';
import { useSetAtom } from 'jotai';
import { sipCallStateAtom } from '@/state/sip-state';
import { useSip } from '@/providers/sip-provider';
-import { useAuth } from '@/providers/auth-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 m = Math.floor(seconds / 60).toString().padStart(2, '0');
const s = (seconds % 60).toString().padStart(2, '0');
return `${m}:${s}`;
};
-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',
- },
-];
-
+// CallWidget is a lightweight floating notification for calls outside the Call Desk.
+// It only handles: ringing (answer/decline) + auto-redirect to Call Desk.
+// All active call management (mute, hold, end, disposition) happens on the Call Desk via ActiveCallCard.
export const CallWidget = () => {
- const {
- callState,
- callerNumber,
- isMuted,
- isOnHold,
- callDuration,
- answer,
- reject,
- hangup,
- toggleMute,
- toggleHold,
- } = useSip();
- const { user } = useAuth();
+ const { callState, callerNumber, callDuration, answer, reject } = useSip();
const setCallState = useSetAtom(sipCallStateAtom);
+ const navigate = useNavigate();
+ const { pathname } = useLocation();
- const [disposition, setDisposition] = useState
(null);
- const [notes, setNotes] = useState('');
- const [lastDuration, setLastDuration] = useState(0);
- const [matchedLead, setMatchedLead] = useState(null);
- const [leadActivities, setLeadActivities] = useState([]);
- const [isSaving, setIsSaving] = useState(false);
- const [isAppointmentOpen, setIsAppointmentOpen] = useState(false);
- const callStartTimeRef = useRef(null);
-
- // Capture duration right before call ends
+ // Auto-navigate to Call Desk when a call becomes active or outbound ringing starts
useEffect(() => {
- if (callState === 'active' && callDuration > 0) {
- setLastDuration(callDuration);
+ if (pathname === '/call-desk') return;
+ if (callState === 'active' || callState === 'ringing-out') {
+ console.log(`[CALL-WIDGET] Redirecting to Call Desk (state=${callState})`);
+ navigate('/call-desk');
}
- }, [callState, callDuration]);
-
- // Track call start time
- useEffect(() => {
- if (callState === 'active' && !callStartTimeRef.current) {
- callStartTimeRef.current = new Date().toISOString();
- }
- if (callState === 'idle') {
- callStartTimeRef.current = null;
- }
- }, [callState]);
-
- // Look up caller when call becomes active
- useEffect(() => {
- if (callState === 'ringing-in' && callerNumber && callerNumber !== 'Unknown') {
- const lookup = async () => {
- try {
- const { apiClient } = await import('@/lib/api-client');
- const token = apiClient.getStoredToken();
- if (!token) return;
-
- const API_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:4100';
- const res = await fetch(`${API_URL}/api/call/lookup`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'Authorization': `Bearer ${token}`,
- },
- body: JSON.stringify({ phoneNumber: callerNumber }),
- });
- const data = await res.json();
- if (data.matched && data.lead) {
- setMatchedLead(data.lead);
- setLeadActivities(data.activities ?? []);
- }
- } catch (err) {
- console.warn('Lead lookup failed:', err);
- }
- };
- lookup();
- }
- }, [callState, callerNumber]);
-
- // Reset state when returning to idle
- useEffect(() => {
- if (callState === 'idle') {
- setDisposition(null);
- setNotes('');
- setMatchedLead(null);
- setLeadActivities([]);
- }
- }, [callState]);
+ }, [callState, pathname, navigate]);
// Auto-dismiss ended/failed state after 3 seconds
useEffect(() => {
@@ -181,127 +48,35 @@ export const CallWidget = () => {
}
}, [callState, setCallState]);
- const handleSaveAndClose = async () => {
- if (!disposition) return;
- console.log(`[CALL-WIDGET] Save & Close: disposition=${disposition} lead=${matchedLead?.id ?? 'none'}`);
- setIsSaving(true);
-
- try {
- const { apiClient } = await import('@/lib/api-client');
-
- // 1. Create Call record on platform
- await apiClient.graphql(
- `mutation CreateCall($data: CallCreateInput!) {
- createCall(data: $data) { id }
- }`,
- {
- data: {
- callDirection: 'INBOUND',
- callStatus: 'COMPLETED',
- agentName: user.name,
- startedAt: callStartTimeRef.current,
- endedAt: new Date().toISOString(),
- durationSeconds: callDuration,
- disposition,
- callNotes: notes || null,
- leadId: matchedLead?.id ?? null,
- },
- },
- ).catch(err => console.warn('Failed to create call record:', err));
-
- // 2. Update lead status if matched
- if (matchedLead?.id) {
- const statusMap: Partial> = {
- APPOINTMENT_BOOKED: 'APPOINTMENT_SET',
- FOLLOW_UP_SCHEDULED: 'CONTACTED',
- INFO_PROVIDED: 'CONTACTED',
- NO_ANSWER: 'CONTACTED',
- WRONG_NUMBER: 'LOST',
- CALLBACK_REQUESTED: 'CONTACTED',
- NOT_INTERESTED: 'LOST',
- };
- const newStatus = statusMap[disposition];
- if (newStatus) {
- await apiClient.graphql(
- `mutation UpdateLead($id: UUID!, $data: LeadUpdateInput!) {
- updateLead(id: $id, data: $data) { id }
- }`,
- {
- id: matchedLead.id,
- data: {
- leadStatus: newStatus,
- lastContactedAt: new Date().toISOString(),
- },
- },
- ).catch(err => console.warn('Failed to update lead:', err));
- }
-
- // 3. Create lead activity
- await apiClient.graphql(
- `mutation CreateLeadActivity($data: LeadActivityCreateInput!) {
- createLeadActivity(data: $data) { id }
- }`,
- {
- data: {
- activityType: 'CALL_RECEIVED',
- summary: `Inbound call — ${disposition.replace(/_/g, ' ')}`,
- occurredAt: new Date().toISOString(),
- performedBy: user.name,
- channel: 'PHONE',
- durationSeconds: callDuration,
- leadId: matchedLead.id,
- },
- },
- ).catch(err => console.warn('Failed to create activity:', err));
- }
- } catch (err) {
- console.error('Save failed:', err);
- }
-
- setIsSaving(false);
- hangup();
- setDisposition(null);
- setNotes('');
- };
-
- // Log state changes for observability
+ // Log state changes
useEffect(() => {
if (callState !== 'idle') {
console.log(`[CALL-WIDGET] State: ${callState} | caller=${callerNumber ?? 'none'}`);
}
}, [callState, callerNumber]);
- // Idle: nothing to show — call desk has its own status toggle
- if (callState === 'idle') {
- return null;
- }
+ if (callState === 'idle') return null;
- // Ringing inbound
+ // Ringing inbound — answer redirects to Call Desk
if (callState === 'ringing-in') {
return (
-
+
-
-
- Incoming Call
-
+ Incoming Call
{callerNumber ?? 'Unknown'}
-
-