import { useState, useCallback, useMemo } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faSparkles, faPhone, faChevronDown, faChevronUp, faCalendarCheck, faClockRotateLeft, faPhoneMissed, faPhoneArrowDown, faPhoneArrowUp, faListCheck, } from '@fortawesome/pro-duotone-svg-icons'; import { AiChatPanel } from './ai-chat-panel'; import { Badge } from '@/components/base/badges/badges'; import { formatPhone, formatShortDate } from '@/lib/format'; import { cx } from '@/utils/cx'; import type { Lead, LeadActivity, Call, FollowUp, Patient, Appointment } from '@/types/entities'; import { AppointmentForm } from './appointment-form'; interface ContextPanelProps { selectedLead: Lead | null; activities: LeadActivity[]; calls: Call[]; followUps: FollowUp[]; appointments: Appointment[]; patients: Patient[]; callerPhone?: string; isInCall?: boolean; callUcid?: string | null; } const formatTimeAgo = (dateStr: string): string => { const minutes = Math.round((Date.now() - new Date(dateStr).getTime()) / 60000); if (minutes < 1) return 'Just now'; if (minutes < 60) return `${minutes}m ago`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h ago`; return `${Math.floor(hours / 24)}d ago`; }; const formatDuration = (sec: number): string => { if (sec < 60) return `${sec}s`; return `${Math.floor(sec / 60)}m ${sec % 60}s`; }; const SectionHeader = ({ icon, label, count, expanded, onToggle }: { icon: any; label: string; count?: number; expanded: boolean; onToggle: () => void; }) => ( {label} {count !== undefined && count > 0 && ( {count} )} ); export const ContextPanel = ({ selectedLead, activities, calls, followUps, appointments, patients, callerPhone, isInCall }: ContextPanelProps) => { const [contextExpanded, setContextExpanded] = useState(true); const [insightExpanded, setInsightExpanded] = useState(true); const [actionsExpanded, setActionsExpanded] = useState(true); const [recentExpanded, setRecentExpanded] = useState(true); const [editingAppointment, setEditingAppointment] = useState(null); const lead = selectedLead; const firstName = lead?.contactName?.firstName ?? ''; const lastName = lead?.contactName?.lastName ?? ''; const fullName = `${firstName} ${lastName}`.trim(); const phone = lead?.contactPhone?.[0]; const callerContext = lead ? { callerPhone: phone?.number ?? callerPhone, leadId: lead.id, leadName: fullName, } : callerPhone ? { callerPhone } : undefined; // Filter data for this lead const leadCalls = useMemo(() => calls.filter(c => c.leadId === lead?.id || (callerPhone && c.callerNumber?.[0]?.number?.endsWith(callerPhone))) .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) .slice(0, 5), [calls, lead, callerPhone], ); const leadFollowUps = useMemo(() => followUps.filter(f => f.patientId === (lead as any)?.patientId && f.followUpStatus !== 'COMPLETED' && f.followUpStatus !== 'CANCELLED') .sort((a, b) => new Date(a.scheduledAt ?? '').getTime() - new Date(b.scheduledAt ?? '').getTime()) .slice(0, 3), [followUps, lead], ); const leadAppointments = useMemo(() => { const patientId = (lead as any)?.patientId; if (!patientId) return []; return appointments .filter(a => a.patientId === patientId && a.appointmentStatus !== 'CANCELLED' && a.appointmentStatus !== 'NO_SHOW') .sort((a, b) => new Date(a.scheduledAt ?? '').getTime() - new Date(b.scheduledAt ?? '').getTime()) .slice(0, 3); }, [appointments, lead]); const leadActivities = useMemo(() => activities.filter(a => a.leadId === lead?.id) .sort((a, b) => new Date(b.occurredAt ?? '').getTime() - new Date(a.occurredAt ?? '').getTime()) .slice(0, 5), [activities, lead], ); // Linked patient const linkedPatient = useMemo(() => patients.find(p => p.id === (lead as any)?.patientId), [patients, lead], ); // Auto-collapse context sections when chat starts const handleChatStart = useCallback(() => { setContextExpanded(false); }, []); const hasContext = !!(lead?.aiSummary || leadCalls.length || leadFollowUps.length || leadAppointments.length || leadActivities.length); return ( {/* Lead header — always visible */} {lead && ( setContextExpanded(!contextExpanded)} className="flex w-full items-center gap-2 px-4 py-2.5 text-left hover:bg-primary_hover transition duration-100 ease-linear" > {isInCall && ( )} {fullName || 'Unknown'} {phone && ( {formatPhone(phone)} )} {lead.leadStatus && ( {lead.leadStatus.replace(/_/g, ' ')} )} {/* Expanded context sections */} {contextExpanded && ( {/* AI Insight */} {lead.aiSummary && ( setInsightExpanded(!insightExpanded)} /> {insightExpanded && ( {lead.aiSummary} {lead.aiSuggestedAction && ( {lead.aiSuggestedAction} )} )} )} {/* Quick Actions — upcoming appointments + follow-ups + linked patient */} {(leadAppointments.length > 0 || leadFollowUps.length > 0 || linkedPatient) && ( setActionsExpanded(!actionsExpanded)} /> {actionsExpanded && ( {leadAppointments.map(appt => ( {appt.doctorName ?? 'Appointment'} {appt.department} {appt.scheduledAt && ( — {formatShortDate(appt.scheduledAt)} )} {appt.appointmentStatus?.replace(/_/g, ' ') ?? 'Scheduled'} setEditingAppointment(appt)} className="text-[11px] font-medium text-brand-secondary hover:text-brand-secondary_hover shrink-0" > Edit ))} {leadFollowUps.map(fu => ( {fu.followUpType?.replace(/_/g, ' ') ?? 'Follow-up'} {fu.scheduledAt && ( {formatShortDate(fu.scheduledAt)} )} {fu.followUpStatus?.replace(/_/g, ' ') ?? 'Pending'} ))} {linkedPatient && ( Patient: {linkedPatient.fullName?.firstName} {linkedPatient.fullName?.lastName} {linkedPatient.patientType && ( {linkedPatient.patientType} )} )} )} )} {/* Recent calls + activities */} {(leadCalls.length > 0 || leadActivities.length > 0) && ( setRecentExpanded(!recentExpanded)} /> {recentExpanded && ( {leadCalls.map(call => ( {call.callStatus === 'MISSED' ? 'Missed' : call.callDirection === 'INBOUND' ? 'Inbound' : 'Outbound'} call {call.durationSeconds != null && call.durationSeconds > 0 && ( — {formatDuration(call.durationSeconds)} )} {call.disposition && ( , {call.disposition.replace(/_/g, ' ')} )} {formatTimeAgo(call.startedAt ?? call.createdAt)} ))} {leadActivities .filter(a => !leadCalls.some(c => a.summary?.includes(c.callerNumber?.[0]?.number ?? '---'))) .slice(0, 3) .map(a => ( {a.summary} {a.occurredAt && ( {formatTimeAgo(a.occurredAt)} )} )) } )} )} {/* No context available */} {!hasContext && ( No history for this lead yet. )} )} )} {/* AI Chat — fills remaining space */} {/* Appointment edit form */} {editingAppointment && ( { if (!open) setEditingAppointment(null); }} callerNumber={callerPhone} leadName={fullName} leadId={lead?.id} patientId={editingAppointment.patientId} existingAppointment={{ id: editingAppointment.id, scheduledAt: editingAppointment.scheduledAt ?? '', doctorName: editingAppointment.doctorName ?? '', doctorId: editingAppointment.doctorId ?? undefined, department: editingAppointment.department ?? '', reasonForVisit: editingAppointment.reasonForVisit ?? undefined, status: editingAppointment.appointmentStatus ?? 'SCHEDULED', }} onSaved={() => setEditingAppointment(null)} /> )} ); };
{lead.aiSummary}
{lead.aiSuggestedAction}
No history for this lead yet.