diff --git a/src/pages/call-desk.tsx b/src/pages/call-desk.tsx index ea11384..4e5ed0a 100644 --- a/src/pages/call-desk.tsx +++ b/src/pages/call-desk.tsx @@ -1,263 +1,50 @@ -import { useState, useCallback, useRef } from 'react'; +import { useAuth } from '@/providers/auth-provider'; +import { useData } from '@/providers/data-provider'; +import { useLeads } from '@/hooks/use-leads'; +import { useSip } from '@/providers/sip-provider'; import { TopBar } from '@/components/layout/top-bar'; -import { CallSimulator } from '@/components/call-desk/call-simulator'; import { IncomingCallCard } from '@/components/call-desk/incoming-call-card'; import { ClickToCallButton } from '@/components/call-desk/click-to-call-button'; import { CallLog } from '@/components/call-desk/call-log'; import { DailyStats } from '@/components/call-desk/daily-stats'; -import { useAuth } from '@/providers/auth-provider'; -import { useData } from '@/providers/data-provider'; -import { useLeads } from '@/hooks/use-leads'; -import { useCallEvents } from '@/hooks/use-call-events'; -import { useSip } from '@/providers/sip-provider'; import { BadgeWithDot } from '@/components/base/badges/badges'; import { formatPhone } from '@/lib/format'; -import type { Call, CallDisposition, Lead, LeadStatus } from '@/types/entities'; - -type CallState = 'idle' | 'ringing' | 'active' | 'completed'; -type Mode = 'demo' | 'live'; const isToday = (dateStr: string): boolean => { const d = new Date(dateStr); const now = new Date(); - return ( - d.getFullYear() === now.getFullYear() && - d.getMonth() === now.getMonth() && - d.getDate() === now.getDate() - ); -}; - -const dispositionToStatus: Partial> = { - APPOINTMENT_BOOKED: 'APPOINTMENT_SET', - FOLLOW_UP_SCHEDULED: 'CONTACTED', - INFO_PROVIDED: 'CONTACTED', - NO_ANSWER: 'NEW', - WRONG_NUMBER: 'LOST', - CALLBACK_REQUESTED: 'LOST', + return d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate(); }; export const CallDeskPage = () => { const { user } = useAuth(); - const { calls, leadActivities, campaigns, addCall } = useData(); - const { leads, updateLead } = useLeads(); - - // Mode toggle: demo (mock simulator) vs live (WebSocket) - const [mode, setMode] = useState('demo'); - - // --- Demo mode state --- - const [localCallState, setLocalCallState] = useState('idle'); - const [localActiveLead, setLocalActiveLead] = useState['leads'][number] | null>(null); - const [completedDisposition, setCompletedDisposition] = useState(null); - const callStartRef = useRef(null); - const ringingTimerRef = useRef | null>(null); - const completedTimerRef = useRef | null>(null); - - // --- SIP phone state --- + const { calls, leadActivities, campaigns } = useData(); + const { leads } = useLeads(); const { connectionStatus, isRegistered } = useSip(); - // --- Live mode state (WebSocket) --- - const { callState: liveCallState, activeLead: liveLead, isConnected, sendDisposition } = useCallEvents(user.name); - - // Effective state based on mode - const effectiveCallState = mode === 'live' ? liveCallState : localCallState; - const todaysCalls = calls.filter( (c) => c.agentName === user.name && c.startedAt !== null && isToday(c.startedAt), ); - // Demo mode: simulate a call - const handleSimulateCall = useCallback(() => { - if (localCallState !== 'idle') return; - - // Prefer leads with aiSummary, fall back to any lead - const leadsWithAi = leads.filter((l) => l.aiSummary !== null); - const pool = leadsWithAi.length > 0 ? leadsWithAi : leads; - if (pool.length === 0) return; - - const randomLead = pool[Math.floor(Math.random() * pool.length)]; - setLocalActiveLead(randomLead); - setLocalCallState('ringing'); - setCompletedDisposition(null); - - ringingTimerRef.current = setTimeout(() => { - setLocalCallState('active'); - callStartRef.current = new Date(); - }, 1500); - }, [localCallState, leads]); - - // Demo mode: log disposition locally - const handleDemoDisposition = useCallback( - (disposition: CallDisposition, notes: string) => { - if (localActiveLead === null) return; - - const now = new Date(); - const startedAt = callStartRef.current ?? now; - const durationSeconds = Math.floor((now.getTime() - startedAt.getTime()) / 1000); - - const newCall: Call = { - id: `call-sim-${Date.now()}`, - createdAt: startedAt.toISOString(), - callDirection: 'INBOUND', - callStatus: 'COMPLETED', - callerNumber: localActiveLead.contactPhone, - agentName: user.name, - startedAt: startedAt.toISOString(), - endedAt: now.toISOString(), - durationSeconds: Math.max(durationSeconds, 60), - recordingUrl: null, - disposition, - callNotes: notes || null, - patientId: null, - appointmentId: null, - leadId: localActiveLead.id, - leadName: - `${localActiveLead.contactName?.firstName ?? ''} ${localActiveLead.contactName?.lastName ?? ''}`.trim() || - 'Unknown', - leadPhone: localActiveLead.contactPhone?.[0]?.number ?? undefined, - leadService: localActiveLead.interestedService ?? undefined, - }; - - addCall(newCall); - - const newStatus = dispositionToStatus[disposition]; - if (newStatus !== undefined) { - updateLead(localActiveLead.id, { - leadStatus: newStatus, - lastContactedAt: now.toISOString(), - contactAttempts: (localActiveLead.contactAttempts ?? 0) + 1, - }); - } - - setCompletedDisposition(disposition); - setLocalCallState('completed'); - - completedTimerRef.current = setTimeout(() => { - setLocalCallState('idle'); - setLocalActiveLead(null); - setCompletedDisposition(null); - }, 3000); - }, - [localActiveLead, user.name, addCall, updateLead], - ); - - // Route disposition to the correct handler based on mode - const handleDisposition = useCallback( - (disposition: CallDisposition, notes: string) => { - if (mode === 'live') { - sendDisposition(disposition, notes); - } else { - handleDemoDisposition(disposition, notes); - } - }, - [mode, sendDisposition, handleDemoDisposition], - ); - - // Build the lead shape for IncomingCallCard — live leads come in EnrichedLead form, map to Lead - const displayLead: Lead | null = (() => { - if (mode === 'live' && liveLead !== null) { - const mapped: Lead = { - id: liveLead.id, - createdAt: liveLead.age ? new Date(Date.now() - liveLead.age * 24 * 60 * 60 * 1000).toISOString() : null, - updatedAt: null, - leadSource: (liveLead.source as Lead['leadSource']) ?? null, - leadStatus: (liveLead.status as Lead['leadStatus']) ?? null, - priority: null, - contactName: { firstName: liveLead.firstName, lastName: liveLead.lastName }, - contactPhone: liveLead.phone ? [{ number: liveLead.phone, callingCode: '' }] : null, - contactEmail: liveLead.email ? [{ address: liveLead.email }] : null, - interestedService: liveLead.interestedService ?? null, - assignedAgent: null, - utmSource: null, - utmMedium: null, - utmCampaign: null, - utmContent: null, - utmTerm: null, - landingPageUrl: null, - referrerUrl: null, - leadScore: null, - spamScore: null, - isSpam: null, - isDuplicate: null, - duplicateOfLeadId: null, - firstContactedAt: null, - lastContactedAt: null, - contactAttempts: null, - convertedAt: null, - aiSummary: liveLead.aiSummary ?? null, - aiSuggestedAction: liveLead.aiSuggestedAction ?? null, - patientId: null, - campaignId: null, - adId: null, - }; - return mapped; - } - return localActiveLead; - })(); - return (
- {/* Mode toggle bar */} -
-
- - -
- -
- - {isRegistered ? 'Phone Ready' : `Phone: ${connectionStatus}`} - - - {mode === 'live' && ( - - {isConnected ? 'Connected to call center' : 'Connecting...'} - - )} -
+ {/* Status bar */} +
+ + {isRegistered ? 'Phone Ready' : `Phone: ${connectionStatus}`} +
- {/* Demo mode simulator button */} - {mode === 'demo' && ( - - )} - {/* Worklist: leads with click-to-call */} - {mode === 'live' && leads.length > 0 && ( + {leads.length > 0 && (

Worklist

@@ -273,21 +60,12 @@ export const CallDeskPage = () => { const phoneNumber = phone?.number ?? ''; return ( -
+
- - {fullName} - - - {phoneDisplay} - + {fullName} + {phoneDisplay} {lead.interestedService !== null && ( - - {lead.interestedService} - + {lead.interestedService} )}
@@ -298,14 +76,16 @@ export const CallDeskPage = () => {
)} + {/* Incoming call card — driven by SIP phone state via the floating CallWidget */} {}} + completedDisposition={null} /> +
diff --git a/src/pages/login.tsx b/src/pages/login.tsx index b7e2595..0f06d07 100644 --- a/src/pages/login.tsx +++ b/src/pages/login.tsx @@ -42,31 +42,26 @@ export const LoginPage = () => { event.preventDefault(); setError(null); - // If email and password are provided, try real auth via sidecar - if (email && password) { - setIsLoading(true); - try { - const { apiClient } = await import('@/lib/api-client'); - await apiClient.login(email, password); - login(); // Also set mock auth state for the role-based UI - navigate('/'); - } catch (err: any) { - // If sidecar is down, fall back to demo mode - console.warn('Real auth failed, falling back to demo mode:', err.message); - setError(err.message); - setIsLoading(false); - } + if (!email || !password) { + setError('Email and password are required'); return; } - // No credentials — demo mode (mock auth) - login(); - navigate('/'); + setIsLoading(true); + try { + const { apiClient } = await import('@/lib/api-client'); + await apiClient.login(email, password); + login(); + navigate('/'); + } catch (err: any) { + setError(err.message); + setIsLoading(false); + } }; const handleGoogleSignIn = () => { - login(); - navigate('/'); + // TODO: implement Google OAuth via sidecar + setError('Google sign-in not yet configured'); }; return ( @@ -229,14 +224,9 @@ export const LoginPage = () => { isLoading={isLoading} className="w-full rounded-xl py-3 font-semibold active:scale-[0.98] transition duration-300 ease-[cubic-bezier(0.4,0,0.2,1)]" > - {email ? 'Sign in' : 'Demo Mode'} + Sign in
- {!email && ( -

- Leave email empty for demo mode with mock data -

- )}