mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
feat: worklist sorting, contextual disposition, context panel redesign, notifications
- Worklist default sort descending (newest first), sortable column headers (PRIORITY, PATIENT, SLA) via React Aria - Contextual disposition: auto-selects based on in-call actions (appointment → APPOINTMENT_BOOKED, enquiry → INFO_PROVIDED, transfer → FOLLOW_UP_SCHEDULED) - Context panel redesign: collapsible AI Insight, Upcoming (appointments + follow-ups + linked patient), Recent (calls + activities) sections; auto-collapse on AI chat start - Appointments added to DataProvider with APPOINTMENTS_QUERY, Appointment type, transform - Notification bell for admin/supervisor: performance alerts (idle time, NPS, conversion thresholds) with toast on load + bell dropdown with dismiss; demo alerts as fallback - Slideout z-index fix: added z-50 to slideout ModalOverlay matching modal component Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,68 +1,74 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSparkles, faCalendarCheck, faPhone, faUser } from '@fortawesome/pro-duotone-svg-icons';
|
||||
import { faIcon } from '@/lib/icon-wrapper';
|
||||
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 { apiClient } from '@/lib/api-client';
|
||||
import { formatPhone, formatShortDate } from '@/lib/format';
|
||||
import type { Lead, LeadActivity } from '@/types/entities';
|
||||
|
||||
const CalendarCheck = faIcon(faCalendarCheck);
|
||||
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;
|
||||
}
|
||||
|
||||
export const ContextPanel = ({ selectedLead, activities, callerPhone, isInCall }: ContextPanelProps) => {
|
||||
const [patientData, setPatientData] = useState<any>(null);
|
||||
const [loadingPatient, setLoadingPatient] = useState(false);
|
||||
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`;
|
||||
};
|
||||
|
||||
// Fetch patient data when lead has a patientId
|
||||
useEffect(() => {
|
||||
const patientId = (selectedLead as any)?.patientId;
|
||||
if (!patientId) {
|
||||
setPatientData(null);
|
||||
return;
|
||||
}
|
||||
const formatDuration = (sec: number): string => {
|
||||
if (sec < 60) return `${sec}s`;
|
||||
return `${Math.floor(sec / 60)}m ${sec % 60}s`;
|
||||
};
|
||||
|
||||
setLoadingPatient(true);
|
||||
apiClient.graphql<{ patients: { edges: Array<{ node: any }> } }>(
|
||||
`query GetPatient($id: UUID!) { patients(filter: { id: { eq: $id } }) { edges { node {
|
||||
id fullName { firstName lastName } dateOfBirth gender
|
||||
phones { primaryPhoneNumber } emails { primaryEmail }
|
||||
appointments(first: 5, orderBy: [{ scheduledAt: DescNullsLast }]) { edges { node {
|
||||
id scheduledAt status doctorName department reasonForVisit
|
||||
} } }
|
||||
calls(first: 5, orderBy: [{ startedAt: DescNullsLast }]) { edges { node {
|
||||
id callStatus disposition direction startedAt durationSec agentName
|
||||
} } }
|
||||
} } } }`,
|
||||
{ id: patientId },
|
||||
{ silent: true },
|
||||
).then(data => {
|
||||
setPatientData(data.patients.edges[0]?.node ?? null);
|
||||
}).catch(() => setPatientData(null))
|
||||
.finally(() => setLoadingPatient(false));
|
||||
}, [(selectedLead as any)?.patientId]);
|
||||
const SectionHeader = ({ icon, label, count, expanded, onToggle }: {
|
||||
icon: any; label: string; count?: number; expanded: boolean; onToggle: () => void;
|
||||
}) => (
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="flex w-full items-center gap-1.5 py-1.5 text-left group"
|
||||
>
|
||||
<FontAwesomeIcon icon={icon} className="size-3 text-fg-quaternary" />
|
||||
<span className="text-[11px] font-bold uppercase tracking-wider text-tertiary">{label}</span>
|
||||
{count !== undefined && count > 0 && (
|
||||
<span className="text-[10px] font-semibold text-brand-secondary bg-brand-primary px-1.5 py-0.5 rounded-full">{count}</span>
|
||||
)}
|
||||
<FontAwesomeIcon
|
||||
icon={expanded ? faChevronUp : faChevronDown}
|
||||
className="size-2.5 text-fg-quaternary ml-auto opacity-0 group-hover:opacity-100 transition duration-100 ease-linear"
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
|
||||
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<Appointment | null>(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 email = lead?.contactEmail?.[0]?.address;
|
||||
|
||||
const leadActivities = activities
|
||||
.filter((a) => lead && a.leadId === lead.id)
|
||||
.sort((a, b) => new Date(b.occurredAt ?? b.createdAt ?? '').getTime() - new Date(a.occurredAt ?? a.createdAt ?? '').getTime())
|
||||
.slice(0, 10);
|
||||
|
||||
const appointments = patientData?.appointments?.edges?.map((e: any) => e.node) ?? [];
|
||||
|
||||
const callerContext = lead ? {
|
||||
callerPhone: phone?.number ?? callerPhone,
|
||||
@@ -70,115 +76,250 @@ export const ContextPanel = ({ selectedLead, activities, callerPhone, isInCall }
|
||||
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 (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Context header — shows caller/lead info when available */}
|
||||
{/* Lead header — always visible */}
|
||||
{lead && (
|
||||
<div className="shrink-0 border-b border-secondary p-4 space-y-3">
|
||||
{/* Call status banner */}
|
||||
{isInCall && (
|
||||
<div className="flex items-center gap-2 rounded-lg bg-success-primary px-3 py-2">
|
||||
<FontAwesomeIcon icon={faPhone} className="size-3 text-fg-success-primary" />
|
||||
<span className="text-xs font-semibold text-success-primary">
|
||||
On call with {fullName || callerPhone || 'Unknown'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Lead profile */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-primary">{fullName || 'Unknown'}</h3>
|
||||
{phone && <p className="text-sm text-secondary">{formatPhone(phone)}</p>}
|
||||
{email && <p className="text-xs text-tertiary">{email}</p>}
|
||||
</div>
|
||||
|
||||
{/* Status badges */}
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{!!patientData && (
|
||||
<Badge size="sm" color="brand" type="pill-color">Returning Patient</Badge>
|
||||
<div className="shrink-0 border-b border-secondary">
|
||||
<button
|
||||
onClick={() => 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 && (
|
||||
<FontAwesomeIcon icon={faPhone} className="size-3 text-fg-success-primary shrink-0" />
|
||||
)}
|
||||
{lead.leadStatus && <Badge size="sm" color="brand">{lead.leadStatus.replace(/_/g, ' ')}</Badge>}
|
||||
{lead.leadSource && <Badge size="sm" color="gray">{lead.leadSource.replace(/_/g, ' ')}</Badge>}
|
||||
</div>
|
||||
<span className="text-sm font-semibold text-primary truncate">{fullName || 'Unknown'}</span>
|
||||
{phone && (
|
||||
<span className="text-xs text-tertiary shrink-0">{formatPhone(phone)}</span>
|
||||
)}
|
||||
{lead.leadStatus && (
|
||||
<Badge size="sm" color="brand" type="pill-color" className="shrink-0">{lead.leadStatus.replace(/_/g, ' ')}</Badge>
|
||||
)}
|
||||
<FontAwesomeIcon
|
||||
icon={contextExpanded ? faChevronUp : faChevronDown}
|
||||
className="size-3 text-fg-quaternary ml-auto shrink-0"
|
||||
/>
|
||||
</button>
|
||||
|
||||
{lead.interestedService && (
|
||||
<p className="text-xs text-secondary">Interested in: {lead.interestedService}</p>
|
||||
)}
|
||||
{/* Expanded context sections */}
|
||||
{contextExpanded && (
|
||||
<div className="px-4 pb-3 space-y-1 overflow-y-auto" style={{ maxHeight: '50vh' }}>
|
||||
{/* AI Insight */}
|
||||
{lead.aiSummary && (
|
||||
<div>
|
||||
<SectionHeader icon={faSparkles} label="AI Insight" expanded={insightExpanded} onToggle={() => setInsightExpanded(!insightExpanded)} />
|
||||
{insightExpanded && (
|
||||
<div className="rounded-lg bg-brand-primary p-2.5 mb-1">
|
||||
<p className="text-xs leading-relaxed text-primary">{lead.aiSummary}</p>
|
||||
{lead.aiSuggestedAction && (
|
||||
<p className="mt-1.5 text-xs font-semibold text-brand-secondary">{lead.aiSuggestedAction}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* AI Insight — live from platform */}
|
||||
{(lead.aiSummary || lead.aiSuggestedAction) && (
|
||||
<div className="rounded-lg bg-brand-primary p-3">
|
||||
<div className="mb-1 flex items-center gap-1.5">
|
||||
<FontAwesomeIcon icon={faSparkles} className="size-3 text-fg-brand-primary" />
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider text-brand-secondary">AI Insight</span>
|
||||
</div>
|
||||
{lead.aiSummary && <p className="text-xs text-primary">{lead.aiSummary}</p>}
|
||||
{lead.aiSuggestedAction && (
|
||||
<p className="mt-1 text-xs font-semibold text-brand-secondary">{lead.aiSuggestedAction}</p>
|
||||
{/* Quick Actions — upcoming appointments + follow-ups + linked patient */}
|
||||
{(leadAppointments.length > 0 || leadFollowUps.length > 0 || linkedPatient) && (
|
||||
<div>
|
||||
<SectionHeader icon={faListCheck} label="Upcoming" count={leadAppointments.length + leadFollowUps.length} expanded={actionsExpanded} onToggle={() => setActionsExpanded(!actionsExpanded)} />
|
||||
{actionsExpanded && (
|
||||
<div className="space-y-1 mb-1">
|
||||
{leadAppointments.map(appt => (
|
||||
<div key={appt.id} className="flex items-center gap-2 rounded-lg bg-secondary px-2.5 py-2">
|
||||
<FontAwesomeIcon icon={faCalendarCheck} className="size-3 text-fg-brand-primary shrink-0" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<span className="text-xs font-medium text-primary">
|
||||
{appt.doctorName ?? 'Appointment'}
|
||||
</span>
|
||||
<span className="text-[11px] text-tertiary ml-1">
|
||||
{appt.department}
|
||||
</span>
|
||||
{appt.scheduledAt && (
|
||||
<span className="text-[11px] text-tertiary ml-1">
|
||||
— {formatShortDate(appt.scheduledAt)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Badge size="sm" color={appt.appointmentStatus === 'COMPLETED' ? 'success' : 'brand'} type="pill-color">
|
||||
{appt.appointmentStatus?.replace(/_/g, ' ') ?? 'Scheduled'}
|
||||
</Badge>
|
||||
<button
|
||||
onClick={() => setEditingAppointment(appt)}
|
||||
className="text-[11px] font-medium text-brand-secondary hover:text-brand-secondary_hover shrink-0"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{leadFollowUps.map(fu => (
|
||||
<div key={fu.id} className="flex items-center gap-2 rounded-lg bg-secondary px-2.5 py-2">
|
||||
<FontAwesomeIcon icon={faClockRotateLeft} className="size-3 text-fg-warning-primary shrink-0" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<span className="text-xs font-medium text-primary">
|
||||
{fu.followUpType?.replace(/_/g, ' ') ?? 'Follow-up'}
|
||||
</span>
|
||||
{fu.scheduledAt && (
|
||||
<span className="text-[11px] text-tertiary ml-1.5">
|
||||
{formatShortDate(fu.scheduledAt)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Badge size="sm" color={fu.followUpStatus === 'OVERDUE' ? 'error' : 'gray'} type="pill-color">
|
||||
{fu.followUpStatus?.replace(/_/g, ' ') ?? 'Pending'}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
{linkedPatient && (
|
||||
<div className="flex items-center gap-2 rounded-lg bg-secondary px-2.5 py-2">
|
||||
<FontAwesomeIcon icon={faPhone} className="size-3 text-fg-success-primary shrink-0" />
|
||||
<span className="text-xs text-primary">
|
||||
Patient: <span className="font-medium">{linkedPatient.fullName?.firstName} {linkedPatient.fullName?.lastName}</span>
|
||||
</span>
|
||||
{linkedPatient.patientType && (
|
||||
<Badge size="sm" color="gray" type="pill-color" className="ml-auto">{linkedPatient.patientType}</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Recent calls + activities */}
|
||||
{(leadCalls.length > 0 || leadActivities.length > 0) && (
|
||||
<div>
|
||||
<SectionHeader
|
||||
icon={faClockRotateLeft}
|
||||
label="Recent"
|
||||
count={leadCalls.length + leadActivities.length}
|
||||
expanded={recentExpanded}
|
||||
onToggle={() => setRecentExpanded(!recentExpanded)}
|
||||
/>
|
||||
{recentExpanded && (
|
||||
<div className="space-y-0.5 mb-1">
|
||||
{leadCalls.map(call => (
|
||||
<div key={call.id} className="flex items-center gap-2 py-1.5 px-1">
|
||||
<FontAwesomeIcon
|
||||
icon={call.callStatus === 'MISSED' ? faPhoneMissed : call.callDirection === 'INBOUND' ? faPhoneArrowDown : faPhoneArrowUp}
|
||||
className={cx('size-3 shrink-0',
|
||||
call.callStatus === 'MISSED' ? 'text-fg-error-primary' :
|
||||
call.callDirection === 'INBOUND' ? 'text-fg-success-secondary' : 'text-fg-brand-secondary'
|
||||
)}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<span className="text-xs text-primary">
|
||||
{call.callStatus === 'MISSED' ? 'Missed' : call.callDirection === 'INBOUND' ? 'Inbound' : 'Outbound'} call
|
||||
</span>
|
||||
{call.durationSeconds != null && call.durationSeconds > 0 && (
|
||||
<span className="text-[11px] text-tertiary ml-1">— {formatDuration(call.durationSeconds)}</span>
|
||||
)}
|
||||
{call.disposition && (
|
||||
<span className="text-[11px] text-tertiary ml-1">, {call.disposition.replace(/_/g, ' ')}</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[11px] text-quaternary shrink-0">
|
||||
{formatTimeAgo(call.startedAt ?? call.createdAt)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{leadActivities
|
||||
.filter(a => !leadCalls.some(c => a.summary?.includes(c.callerNumber?.[0]?.number ?? '---')))
|
||||
.slice(0, 3)
|
||||
.map(a => (
|
||||
<div key={a.id} className="flex items-center gap-2 py-1.5 px-1">
|
||||
<span className="size-1.5 rounded-full bg-fg-quaternary shrink-0" />
|
||||
<span className="text-xs text-tertiary truncate flex-1">{a.summary}</span>
|
||||
{a.occurredAt && (
|
||||
<span className="text-[11px] text-quaternary shrink-0">{formatTimeAgo(a.occurredAt)}</span>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* No context available */}
|
||||
{!hasContext && (
|
||||
<p className="text-xs text-quaternary py-2">No history for this lead yet.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Upcoming appointments */}
|
||||
{appointments.length > 0 && (
|
||||
<div>
|
||||
<span className="text-[10px] font-bold text-tertiary uppercase">Appointments</span>
|
||||
<div className="mt-1 space-y-1">
|
||||
{appointments.slice(0, 3).map((appt: any) => (
|
||||
<div key={appt.id} className="flex items-center gap-2 rounded-md bg-secondary px-2 py-1.5">
|
||||
<CalendarCheck className="size-3 text-fg-brand-primary shrink-0" />
|
||||
<span className="text-xs text-primary truncate">
|
||||
{appt.doctorName ?? 'Doctor'} · {appt.scheduledAt ? formatShortDate(appt.scheduledAt) : ''}
|
||||
</span>
|
||||
{appt.status && (
|
||||
<Badge size="sm" color={appt.status === 'COMPLETED' ? 'success' : appt.status === 'CANCELLED' ? 'error' : 'brand'}>
|
||||
{appt.status.toLowerCase()}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loadingPatient && <p className="text-[10px] text-quaternary">Loading patient details...</p>}
|
||||
|
||||
{/* Recent activity */}
|
||||
{leadActivities.length > 0 && (
|
||||
<div>
|
||||
<span className="text-[10px] font-bold text-tertiary uppercase">Recent Activity</span>
|
||||
<div className="mt-1 space-y-1">
|
||||
{leadActivities.slice(0, 5).map((a) => (
|
||||
<div key={a.id} className="flex items-start gap-2">
|
||||
<div className="mt-1.5 size-1.5 shrink-0 rounded-full bg-fg-quaternary" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs text-primary truncate">{a.summary}</p>
|
||||
<p className="text-[10px] text-quaternary">
|
||||
{a.activityType?.replace(/_/g, ' ')}{a.occurredAt ? ` · ${formatShortDate(a.occurredAt)}` : ''}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* No lead selected — empty state */}
|
||||
{!lead && (
|
||||
<div className="shrink-0 flex items-center justify-center border-b border-secondary px-4 py-6">
|
||||
<div className="text-center">
|
||||
<FontAwesomeIcon icon={faUser} className="mb-2 size-6 text-fg-quaternary" />
|
||||
<p className="text-xs text-tertiary">Select a lead from the worklist to see context</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* AI Chat — always available at the bottom */}
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
<AiChatPanel callerContext={callerContext} />
|
||||
{/* AI Chat — fills remaining space */}
|
||||
<div className="flex flex-1 flex-col min-h-0 overflow-hidden">
|
||||
<AiChatPanel callerContext={callerContext} onChatStart={handleChatStart} />
|
||||
</div>
|
||||
|
||||
{/* Appointment edit form */}
|
||||
{editingAppointment && (
|
||||
<AppointmentForm
|
||||
isOpen={!!editingAppointment}
|
||||
onOpenChange={(open) => { 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)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user