diff --git a/src/components/call-desk/active-call-card.tsx b/src/components/call-desk/active-call-card.tsx index e81544b..ec23330 100644 --- a/src/components/call-desk/active-call-card.tsx +++ b/src/components/call-desk/active-call-card.tsx @@ -1,10 +1,22 @@ +import { useState } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faPhone, faPhoneHangup, faMicrophone, faMicrophoneSlash, faPause, faPlay } from '@fortawesome/pro-duotone-svg-icons'; +import { + faPhone, faPhoneHangup, faMicrophone, faMicrophoneSlash, + faPause, faPlay, faCalendarPlus, faCheckCircle, +} from '@fortawesome/pro-duotone-svg-icons'; import { Button } from '@/components/base/buttons/button'; import { Badge } from '@/components/base/badges/badges'; +import { useSetAtom } from 'jotai'; +import { sipCallStateAtom, sipCallerNumberAtom } from '@/state/sip-state'; import { useSip } from '@/providers/sip-provider'; +import { DispositionForm } from './disposition-form'; +import { AppointmentForm } from './appointment-form'; import { formatPhone } from '@/lib/format'; -import type { Lead } from '@/types/entities'; +import { apiClient } from '@/lib/api-client'; +import { notify } from '@/lib/toast'; +import type { Lead, CallDisposition } from '@/types/entities'; + +type PostCallStage = 'disposition' | 'appointment' | 'follow-up' | 'done'; interface ActiveCallCardProps { lead: Lead | null; @@ -19,6 +31,11 @@ const formatDuration = (seconds: number): string => { export const ActiveCallCard = ({ lead, callerPhone }: ActiveCallCardProps) => { const { callState, callDuration, isMuted, isOnHold, answer, reject, hangup, toggleMute, toggleHold } = useSip(); + const setCallState = useSetAtom(sipCallStateAtom); + const setCallerNumber = useSetAtom(sipCallerNumberAtom); + const [postCallStage, setPostCallStage] = useState(null); + const [savedDisposition, setSavedDisposition] = useState(null); + const [appointmentOpen, setAppointmentOpen] = useState(false); const firstName = lead?.contactName?.firstName ?? ''; const lastName = lead?.contactName?.lastName ?? ''; @@ -26,6 +43,69 @@ export const ActiveCallCard = ({ lead, callerPhone }: ActiveCallCardProps) => { const phone = lead?.contactPhone?.[0]; const phoneDisplay = phone ? formatPhone(phone) : callerPhone || 'Unknown'; + const handleDisposition = async (disposition: CallDisposition, notes: string) => { + setSavedDisposition(disposition); + + // Create call record in platform + try { + await apiClient.graphql(`mutation($data: CallCreateInput!) { createCall(data: $data) { id } }`, { + data: { + name: `${fullName || phoneDisplay} — ${disposition}`, + direction: 'INBOUND', + callStatus: 'COMPLETED', + agentName: null, + startedAt: new Date().toISOString(), + durationSec: callDuration, + disposition, + leadId: lead?.id ?? null, + }, + }, { silent: true }); + } catch { + // non-blocking + } + + if (disposition === 'APPOINTMENT_BOOKED') { + setPostCallStage('appointment'); + setAppointmentOpen(true); + } else if (disposition === 'FOLLOW_UP_SCHEDULED') { + setPostCallStage('follow-up'); + // Create follow-up + try { + await apiClient.graphql(`mutation($data: FollowUpCreateInput!) { createFollowUp(data: $data) { id } }`, { + data: { + name: `Follow-up — ${fullName || phoneDisplay}`, + typeCustom: 'CALLBACK', + status: 'PENDING', + assignedAgent: null, + priority: 'NORMAL', + scheduledAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + }, + }, { silent: true }); + notify.success('Follow-up Created', 'Callback scheduled for tomorrow'); + } catch { + notify.info('Follow-up', 'Could not auto-create follow-up'); + } + setPostCallStage('done'); + } else { + notify.success('Call Logged', `Disposition: ${disposition.replace(/_/g, ' ').toLowerCase()}`); + setPostCallStage('done'); + } + }; + + const handleAppointmentSaved = () => { + setAppointmentOpen(false); + notify.success('Appointment Booked', 'Payment link will be sent to the patient'); + setPostCallStage('done'); + }; + + const handleReset = () => { + setPostCallStage(null); + setSavedDisposition(null); + setCallState('idle'); + setCallerNumber(null); + }; + + // Ringing state if (callState === 'ringing-in') { return (
@@ -43,17 +123,14 @@ export const ActiveCallCard = ({ lead, callerPhone }: ActiveCallCardProps) => {
- - + +
); } + // Active call if (callState === 'active') { return (
@@ -70,38 +147,88 @@ export const ActiveCallCard = ({ lead, callerPhone }: ActiveCallCardProps) => { {formatDuration(callDuration)}
- - - + + + +
+ + {/* Appointment form accessible during call */} + + + ); + } + + // Call ended — show disposition + if (callState === 'ended' || callState === 'failed' || postCallStage !== null) { + // Done state + if (postCallStage === 'done') { + return ( +
+ +

Call Completed

+

+ {savedDisposition ? savedDisposition.replace(/_/g, ' ').toLowerCase() : 'logged'} +

+
+ ); + } + + // Appointment booking after disposition + if (postCallStage === 'appointment') { + return ( + <> +
+ +

Booking Appointment

+

for {fullName || phoneDisplay}

+
+ { + setAppointmentOpen(open); + if (!open) setPostCallStage('done'); + }} + callerNumber={callerPhone} + leadName={fullName || null} + leadId={lead?.id ?? null} + onSaved={handleAppointmentSaved} + /> + + ); + } + + // Disposition form + return ( +
+
+
+ +
+
+

Call Ended — {fullName || phoneDisplay}

+

{formatDuration(callDuration)} · Log this call

+
+
+
); } diff --git a/src/components/dashboard/kpi-cards.tsx b/src/components/dashboard/kpi-cards.tsx index e3feb26..451ed3d 100644 --- a/src/components/dashboard/kpi-cards.tsx +++ b/src/components/dashboard/kpi-cards.tsx @@ -80,7 +80,8 @@ export const DashboardKpi = ({ calls, leads }: DashboardKpiProps) => { const leadsWithResponse = leads.filter((l) => l.createdAt && l.firstContactedAt); const avgResponseTime = leadsWithResponse.length > 0 ? Math.round(leadsWithResponse.reduce((sum, l) => { - return sum + (new Date(l.firstContactedAt!).getTime() - new Date(l.createdAt!).getTime()) / 60000; + const diff = Math.abs(new Date(l.firstContactedAt!).getTime() - new Date(l.createdAt!).getTime()) / 60000; + return sum + diff; }, 0) / leadsWithResponse.length) : null; diff --git a/src/pages/call-desk.tsx b/src/pages/call-desk.tsx index 7bcca6c..00f91e6 100644 --- a/src/pages/call-desk.tsx +++ b/src/pages/call-desk.tsx @@ -21,7 +21,7 @@ export const CallDeskPage = () => { const [selectedLead, setSelectedLead] = useState(null); const [contextOpen, setContextOpen] = useState(true); - const isInCall = callState === 'ringing-in' || callState === 'ringing-out' || callState === 'active'; + const isInCall = callState === 'ringing-in' || callState === 'ringing-out' || callState === 'active' || callState === 'ended' || callState === 'failed'; const callerLead = callerNumber ? marketingLeads.find((l) => l.contactPhone?.[0]?.number?.endsWith(callerNumber) || callerNumber.endsWith(l.contactPhone?.[0]?.number ?? '---')) diff --git a/src/pages/campaigns.tsx b/src/pages/campaigns.tsx index 74b37e1..d243c2c 100644 --- a/src/pages/campaigns.tsx +++ b/src/pages/campaigns.tsx @@ -115,7 +115,7 @@ export const CampaignsPage = () => { leads={leadsByCampaign.get(campaign.id) ?? []} /> -
+