diff --git a/src/components/call-desk/call-widget.tsx b/src/components/call-desk/call-widget.tsx index d95303d..592bab9 100644 --- a/src/components/call-desk/call-widget.tsx +++ b/src/components/call-desk/call-widget.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { Phone01, PhoneIncoming01, @@ -101,24 +101,146 @@ export const CallWidget = () => { 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 callStartTimeRef = useRef(null); - // Capture duration right before call ends so we can display it in the ended state + // Capture duration right before call ends useEffect(() => { if (callState === 'active' && callDuration > 0) { setLastDuration(callDuration); } }, [callState, callDuration]); - // Reset disposition/notes when returning to idle + // 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]); - const handleSaveAndClose = () => { - // TODO: Wire to BFF to create Call record + update Lead + const handleSaveAndClose = async () => { + if (!disposition) return; + 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: 'Rekha S.', + 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: ID!, $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: 'Rekha S.', + 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(''); @@ -227,8 +349,42 @@ export const CallWidget = () => { - {/* Caller number */} - {callerNumber ?? 'Unknown'} + {/* Caller info */} +
+ + {matchedLead?.contactName + ? `${matchedLead.contactName.firstName ?? ''} ${matchedLead.contactName.lastName ?? ''}`.trim() + : callerNumber ?? 'Unknown'} + + {matchedLead && ( + {callerNumber} + )} +
+ + {/* AI Summary */} + {matchedLead?.aiSummary && ( +
+
AI Insight
+

{matchedLead.aiSummary}

+ {matchedLead.aiSuggestedAction && ( + + {matchedLead.aiSuggestedAction} + + )} +
+ )} + + {/* Recent activity */} + {leadActivities.length > 0 && ( +
+
Recent Activity
+ {leadActivities.slice(0, 3).map((a: any, i: number) => ( +
+ {a.activityType?.replace(/_/g, ' ')}: {a.summary} +
+ ))} +
+ )} {/* Call controls */}
@@ -290,11 +446,12 @@ export const CallWidget = () => { size="sm" color="primary" iconLeading={Save01} - isDisabled={disposition === null} + isDisabled={disposition === null || isSaving} + isLoading={isSaving} onClick={handleSaveAndClose} className="w-full" > - Save & Close + {isSaving ? 'Saving...' : 'Save & Close'}