import { useState, useEffect, useRef, useCallback } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faSidebarFlip, faSidebar, faPhone, faXmark, faDeleteLeft, faFlask, faMagnifyingGlass, faCircleInfo } from '@fortawesome/pro-duotone-svg-icons'; import { useSetAtom } from 'jotai'; import { sipCallStateAtom, sipCallerNumberAtom, sipCallUcidAtom, sipCallDurationAtom } from '@/state/sip-state'; import { useAuth } from '@/providers/auth-provider'; import { useData } from '@/providers/data-provider'; import { useWorklist } from '@/hooks/use-worklist'; import { useSip } from '@/providers/sip-provider'; import { WorklistPanel } from '@/components/call-desk/worklist-panel'; import type { WorklistSelection } from '@/components/call-desk/worklist-panel'; import { ContextPanel } from '@/components/call-desk/context-panel'; import type { ContextPanelSubject } from '@/components/call-desk/context-panel'; import { ActiveCallCard } from '@/components/call-desk/active-call-card'; import { Input } from '@/components/base/input/input'; import { faIcon } from '@/lib/icon-wrapper'; import { apiClient } from '@/lib/api-client'; import { notify } from '@/lib/toast'; import { cx } from '@/utils/cx'; const SearchLg = faIcon(faMagnifyingGlass); export const CallDeskPage = () => { const { user } = useAuth(); const { leadActivities, calls, followUps: dataFollowUps, patients, appointments } = useData(); const { callState, callerNumber, callUcid, dialOutbound, isRegistered } = useSip(); const { missedCalls, followUps, marketingLeads, loading } = useWorklist(); const [selectedLead, setSelectedLead] = useState(null); const [selectedItemId, setSelectedItemId] = useState(null); const [contextOpen, setContextOpen] = useState(true); const [activeMissedCallId, setActiveMissedCallId] = useState(null); const [callDismissed, setCallDismissed] = useState(false); const [diallerOpen, setDiallerOpen] = useState(false); const [dialNumber, setDialNumber] = useState(''); const [dialling, setDialling] = useState(false); const [search, setSearch] = useState(''); // DEV: simulate incoming call const setSimCallState = useSetAtom(sipCallStateAtom); const setSimCallerNumber = useSetAtom(sipCallerNumberAtom); const setSimCallUcid = useSetAtom(sipCallUcidAtom); const setSimDuration = useSetAtom(sipCallDurationAtom); const simTimerRef = useRef | null>(null); const startSimCall = useCallback(() => { setSimCallerNumber('+919959966676'); setSimCallUcid(`SIM-${Date.now()}`); setSimDuration(0); setSimCallState('active'); simTimerRef.current = setInterval(() => setSimDuration((d) => d + 1), 1000); }, [setSimCallState, setSimCallerNumber, setSimCallUcid, setSimDuration]); const endSimCall = useCallback(() => { if (simTimerRef.current) { clearInterval(simTimerRef.current); simTimerRef.current = null; } setSimCallState('idle'); setSimCallerNumber(null); setSimCallUcid(null); setSimDuration(0); }, [setSimCallState, setSimCallerNumber, setSimCallUcid, setSimDuration]); const handleDial = async () => { const num = dialNumber.replace(/[^0-9]/g, ''); if (num.length < 10) { notify.error('Enter a valid phone number'); return; } setDialling(true); try { await dialOutbound(num); setDiallerOpen(false); setDialNumber(''); } catch { notify.error('Dial failed'); } finally { setDialling(false); } }; // Reset callDismissed when a new call starts (ringing in or out) if (callDismissed && (callState === 'ringing-in' || callState === 'ringing-out')) { setCallDismissed(false); } const isInCall = !callDismissed && (callState === 'ringing-in' || callState === 'ringing-out' || callState === 'active' || callState === 'ended' || callState === 'failed'); // Resolve caller identity via sidecar (lookup-or-create lead+patient pair) const [resolvedCaller, setResolvedCaller] = useState<{ leadId: string; patientId: string; firstName: string; lastName: string; phone: string; } | null>(null); const resolveAttemptedRef = useRef(null); useEffect(() => { if (!callerNumber || !isInCall) return; if (resolveAttemptedRef.current === callerNumber) return; // already resolving/resolved this number resolveAttemptedRef.current = callerNumber; apiClient.post<{ leadId: string; patientId: string; firstName: string; lastName: string; phone: string; isNew: boolean; }>('/api/caller/resolve', { phone: callerNumber }, { silent: true }) .then((result) => { setResolvedCaller(result); if (result.isNew) { notify.info('New Caller', 'No existing records found for this number'); } }) .catch((err) => { console.warn('[RESOLVE] Caller resolution failed:', err); resolveAttemptedRef.current = null; // allow retry }); }, [callerNumber, isInCall]); // Reset resolved caller when call ends useEffect(() => { if (!isInCall) { setResolvedCaller(null); resolveAttemptedRef.current = null; } }, [isInCall]); // Build activeLead from resolved caller or fallback to client-side match. // The resolver is the authoritative source for patientId (it just joined // lead↔patient by phone), so overlay it on top of any worklist row that // pre-dates the linkage. Without this, the Book Appt pills can't find // a returning caller's prior appointments because the frontend loses // sight of which patient they are. const workLead = resolvedCaller ? marketingLeads.find((l) => l.id === resolvedCaller.leadId) : null; const callerLead = resolvedCaller ? workLead ? { ...workLead, patientId: (workLead as any).patientId ?? resolvedCaller.patientId } : { id: resolvedCaller.leadId, contactName: { firstName: resolvedCaller.firstName, lastName: resolvedCaller.lastName }, contactPhone: [{ number: resolvedCaller.phone, callingCode: '+91' }], patientId: resolvedCaller.patientId, } : callerNumber ? marketingLeads.find((l) => l.contactPhone?.[0]?.number?.endsWith(callerNumber) || callerNumber.endsWith(l.contactPhone?.[0]?.number ?? '---')) : null; // For inbound calls, use resolved/matched lead. For outbound, use selectedLead. const activeLead = isInCall ? (callerLead ?? (callState === 'ringing-out' ? selectedLead : null)) : selectedLead; const activeLeadFull = activeLead as any; // Handle selection from any worklist row type. Leads use the lead // object directly; missed calls and follow-ups build a synthetic // lead-like object from their phone/patientId so the P360 context // panel can render for any row type. const handleSelectItem = useCallback((selection: WorklistSelection) => { setSelectedItemId(selection.rowId); if (selection.lead) { // Lead row — use the full lead object as before setSelectedLead(selection.lead); return; } // Non-lead row (missed call, follow-up, callback) — build a // ContextPanelSubject from the row's available data. The panel // uses contactPhone for call-history matching and patientId for // appointment/follow-up lookups. No type cast needed — the // ContextPanelSubject type accepts these optional fields. const phone = selection.phoneRaw ? selection.phoneRaw.replace(/\D/g, '').slice(-10) : ''; const subject: ContextPanelSubject = { id: selection.leadId ?? selection.rowId, contactName: { firstName: selection.name.split(' ')[0] || '', lastName: selection.name.split(' ').slice(1).join(' ') || '' }, contactPhone: phone ? [{ number: phone, callingCode: '+91' }] : [], patientId: selection.patientId, }; setSelectedLead(subject); }, []); return (
{/* Header — matches PageHeader visual pattern */}

Call Desk

{user.name}
{!isInCall && (
)} {import.meta.env.DEV && (!isInCall ? ( ) : callUcid?.startsWith('SIM-') && ( ))} {!isInCall && (
{diallerOpen && (
Dial
setDialNumber(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleDial()} placeholder="Enter number" autoFocus className="flex-1 bg-transparent text-lg font-semibold text-primary tracking-wider text-center placeholder:text-placeholder placeholder:font-normal placeholder:text-sm outline-none" /> {dialNumber && ( )}
{['1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#'].map(key => ( ))}
)}
)}
{/* Main content */}
{/* Main panel */}
{/* Active call */} {isInCall && (
{ setActiveMissedCallId(null); setCallDismissed(true); }} />
)} {/* Worklist — no wrapper, tabs + table fill the space */} {!isInCall && ( setActiveMissedCallId(id)} search={search} /> )}
{/* Context panel — collapsible with smooth transition */}
{contextOpen && ( )}
); };