import { useEffect, useMemo, useState } from 'react'; import { useParams } from 'react-router'; import { faPhone, faEnvelope, faCalendar, faCommentDots, faPlus, faCalendarCheck } from '@fortawesome/pro-duotone-svg-icons'; import { faIcon } from '@/lib/icon-wrapper'; const Phone01 = faIcon(faPhone); const Mail01 = faIcon(faEnvelope); const Calendar = faIcon(faCalendar); const MessageTextSquare01 = faIcon(faCommentDots); const Plus = faIcon(faPlus); const CalendarCheck = faIcon(faCalendarCheck); import { Tabs, TabList, Tab, TabPanel } from '@/components/application/tabs/tabs'; import { TopBar } from '@/components/layout/top-bar'; import { Avatar } from '@/components/base/avatar/avatar'; import { Badge } from '@/components/base/badges/badges'; import { Button } from '@/components/base/buttons/button'; import { ClickToCallButton } from '@/components/call-desk/click-to-call-button'; import { AppointmentForm } from '@/components/call-desk/appointment-form'; import { apiClient } from '@/lib/api-client'; import { formatShortDate, getInitials } from '@/lib/format'; import { notify } from '@/lib/toast'; import { cx } from '@/utils/cx'; import type { LeadActivity, LeadActivityType, Call, CallDisposition } from '@/types/entities'; // Activity config for timeline (reused from lead-activity-slideout pattern) type ActivityConfig = { icon: string; dotClass: string; label: string; }; const ACTIVITY_CONFIG: Record = { STATUS_CHANGE: { icon: '๐Ÿ”„', dotClass: 'bg-brand-secondary', label: 'Status Changed' }, CALL_MADE: { icon: '๐Ÿ“ž', dotClass: 'bg-brand-secondary', label: 'Call Made' }, CALL_RECEIVED: { icon: '๐Ÿ“ฒ', dotClass: 'bg-brand-secondary', label: 'Call Received' }, WHATSAPP_SENT: { icon: '๐Ÿ’ฌ', dotClass: 'bg-success-solid', label: 'WhatsApp Sent' }, WHATSAPP_RECEIVED: { icon: '๐Ÿ’ฌ', dotClass: 'bg-success-solid', label: 'WhatsApp Received' }, SMS_SENT: { icon: 'โœ‰๏ธ', dotClass: 'bg-brand-secondary', label: 'SMS Sent' }, EMAIL_SENT: { icon: '๐Ÿ“ง', dotClass: 'bg-brand-secondary', label: 'Email Sent' }, EMAIL_RECEIVED: { icon: '๐Ÿ“ง', dotClass: 'bg-brand-secondary', label: 'Email Received' }, NOTE_ADDED: { icon: '๐Ÿ“', dotClass: 'bg-warning-solid', label: 'Note Added' }, ASSIGNED: { icon: '๐Ÿ“ค', dotClass: 'bg-brand-secondary', label: 'Assigned' }, APPOINTMENT_BOOKED: { icon: '๐Ÿ“…', dotClass: 'bg-brand-secondary', label: 'Appointment Booked' }, FOLLOW_UP_CREATED: { icon: '๐Ÿ”', dotClass: 'bg-brand-secondary', label: 'Follow-up Created' }, CONVERTED: { icon: 'โœ…', dotClass: 'bg-success-solid', label: 'Converted' }, MARKED_SPAM: { icon: '๐Ÿšซ', dotClass: 'bg-error-solid', label: 'Marked as Spam' }, DUPLICATE_DETECTED: { icon: '๐Ÿ”', dotClass: 'bg-warning-solid', label: 'Duplicate Detected' }, }; const DEFAULT_CONFIG: ActivityConfig = { icon: '๐Ÿ“Œ', dotClass: 'bg-tertiary', label: 'Activity' }; const DISPOSITION_COLORS: Record = { APPOINTMENT_BOOKED: 'success', APPOINTMENT_RESCHEDULED: 'warning', APPOINTMENT_CANCELLED: 'error', FOLLOW_UP_SCHEDULED: 'brand', INFO_PROVIDED: 'blue', WRONG_NUMBER: 'error', NO_ANSWER: 'warning', NOT_INTERESTED: 'error', CALLBACK_REQUESTED: 'gray', CALL_DROPPED: 'gray', }; const TABS = [ { id: 'appointments', label: 'Appointments' }, { id: 'calls', label: 'Calls' }, { id: 'timeline', label: 'Timeline' }, { id: 'notes', label: 'Notes' }, ]; const PATIENT_QUERY = `query GetPatient360($id: UUID!) { patients(filter: { id: { eq: $id } }) { edges { node { id fullName { firstName lastName } dateOfBirth gender phones { primaryPhoneNumber } emails { primaryEmail } patientType appointments(orderBy: [{ scheduledAt: DescNullsLast }]) { edges { node { id scheduledAt durationMin appointmentType status doctorName department reasonForVisit } } } calls(first: 20, orderBy: [{ startedAt: DescNullsLast }]) { edges { node { id callStatus disposition direction agentName startedAt durationSec callerNumber { primaryPhoneNumber } } } } leads { edges { node { id source status interestedService aiSummary } } } } } } }`; type PatientData = { id: string; fullName: { firstName: string; lastName: string } | null; dateOfBirth: string | null; gender: string | null; phones: { primaryPhoneNumber: string } | null; emails: { primaryEmail: string } | null; patientType: string | null; appointments: { edges: Array<{ node: any }> }; calls: { edges: Array<{ node: any }> }; leads: { edges: Array<{ node: any }> }; }; // Appointment row component const AppointmentRow = ({ appt, onEdit }: { appt: any; onEdit?: (appt: any) => void }) => { const scheduledAt = appt.scheduledAt ? formatShortDate(appt.scheduledAt) : '--'; const statusColors: Record = { COMPLETED: 'success', SCHEDULED: 'brand', CONFIRMED: 'brand', CANCELLED: 'error', NO_SHOW: 'warning', RESCHEDULED: 'warning', }; const canEdit = appt.status !== 'COMPLETED' && appt.status !== 'CANCELLED' && appt.status !== 'NO_SHOW'; return (
canEdit && onEdit?.(appt)}>
{scheduledAt} {appt.appointmentType && ( {appt.appointmentType.replace(/_/g, ' ').toLowerCase().replace(/\b\w/g, (c: string) => c.toUpperCase())} )}

{appt.doctorName ?? 'Unknown Doctor'} {appt.department ? ` ยท ${appt.department}` : ''} {appt.durationMin ? ` ยท ${appt.durationMin}min` : ''}

{appt.reasonForVisit && (

{appt.reasonForVisit}

)}
{appt.status && ( {appt.status.toLowerCase().replace(/\b\w/g, (c: string) => c.toUpperCase())} )}
); }; const formatDuration = (seconds: number | null): string => { if (seconds === null || seconds === 0) return '--'; const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; if (minutes === 0) return `${remainingSeconds}s`; return `${minutes}m ${remainingSeconds}s`; }; const formatDisposition = (disposition: string): string => disposition .toLowerCase() .replace(/_/g, ' ') .replace(/\b\w/g, (c) => c.toUpperCase()); // Timeline item component const TimelineItem = ({ activity, isLast }: { activity: LeadActivity; isLast: boolean }) => { const type = activity.activityType; const config = type ? (ACTIVITY_CONFIG[type] ?? DEFAULT_CONFIG) : DEFAULT_CONFIG; const occurredAt = activity.occurredAt ? formatShortDate(activity.occurredAt) : ''; return (
{!isLast &&
}
{activity.summary ?? config.label} {type === 'STATUS_CHANGE' && (activity.previousValue || activity.newValue) && ( {activity.previousValue && ( {activity.previousValue} )} {activity.previousValue && activity.newValue && 'โ†’ '} {activity.newValue && {activity.newValue}} )} {type !== 'STATUS_CHANGE' && activity.newValue && (

{activity.newValue}

)}

{occurredAt} {activity.performedBy ? ` ยท by ${activity.performedBy}` : ''}

); }; // Call row component const CallRow = ({ call }: { call: Call }) => { const startedAt = call.startedAt ? formatShortDate(call.startedAt) : '--'; const directionLabel = call.callDirection === 'INBOUND' ? 'Inbound' : 'Outbound'; const directionColor = call.callDirection === 'INBOUND' ? 'blue' : 'brand'; return (
{startedAt} {directionLabel}

{call.agentName ?? 'Unknown Agent'} {' ยท '} {formatDuration(call.durationSeconds)}

{call.disposition && ( {formatDisposition(call.disposition)} )}
); }; // Note item component const NoteItem = ({ activity }: { activity: LeadActivity }) => { const occurredAt = activity.occurredAt ? formatShortDate(activity.occurredAt) : ''; return (
๐Ÿ“
{activity.summary ?? 'Note'} {activity.newValue && (

{activity.newValue}

)}

{occurredAt} {activity.performedBy ? ` ยท by ${activity.performedBy}` : ''}

); }; // Empty state component const EmptyState = ({ icon, title, subtitle }: { icon: string; title: string; subtitle: string }) => (
{icon}

{title}

{subtitle}

); export const Patient360Page = () => { const { id } = useParams<{ id: string }>(); const [activeTab, setActiveTab] = useState('appointments'); const [noteText, setNoteText] = useState(''); const [noteSaving, setNoteSaving] = useState(false); const [apptFormOpen, setApptFormOpen] = useState(false); const [editingAppt, setEditingAppt] = useState(null); const [patient, setPatient] = useState(null); const [loading, setLoading] = useState(true); const [activities, setActivities] = useState([]); // Fetch patient with related data from platform useEffect(() => { if (!id) return; setLoading(true); apiClient.graphql<{ patients: { edges: Array<{ node: PatientData }> } }>( PATIENT_QUERY, { id }, { silent: true }, ).then(data => { const p = data.patients.edges[0]?.node ?? null; setPatient(p); // Fetch activities from linked leads const leadIds = p?.leads?.edges?.map((e: any) => e.node.id) ?? []; if (leadIds.length > 0) { const leadFilter = leadIds.map((lid: string) => `"${lid}"`).join(', '); apiClient.graphql<{ leadActivities: { edges: Array<{ node: LeadActivity }> } }>( `{ leadActivities(first: 50, filter: { leadId: { in: [${leadFilter}] } }, orderBy: [{ occurredAt: DescNullsLast }]) { edges { node { id activityType summary occurredAt performedBy previousValue newValue leadId } } } }`, undefined, { silent: true }, ).then(actData => { setActivities(actData.leadActivities.edges.map(e => e.node)); }).catch(() => {}); } }).catch(() => setPatient(null)) .finally(() => setLoading(false)); }, [id]); const patientCalls = useMemo( () => (patient?.calls?.edges?.map(e => e.node) ?? []).map((c: any) => ({ ...c, callDirection: c.direction, durationSeconds: c.durationSec, })), [patient], ); const appointments = useMemo( () => patient?.appointments?.edges?.map(e => e.node) ?? [], [patient], ); const notes = useMemo( () => activities.filter((a) => a.activityType === 'NOTE_ADDED'), [activities], ); const leadInfo = patient?.leads?.edges?.[0]?.node; if (loading) { return ( <>

Loading patient profile...

); } if (!patient) { return ( <>

Patient not found.

); } const firstName = patient.fullName?.firstName ?? ''; const lastName = patient.fullName?.lastName ?? ''; const fullName = `${firstName} ${lastName}`.trim() || 'Unknown Patient'; const initials = getInitials(firstName || '?', lastName || '?'); const phoneRaw = patient.phones?.primaryPhoneNumber ?? ''; const email = patient.emails?.primaryEmail ?? null; const age = patient.dateOfBirth ? Math.floor((Date.now() - new Date(patient.dateOfBirth).getTime()) / (365.25 * 24 * 60 * 60 * 1000)) : null; const genderLabel = patient.gender === 'MALE' ? 'Male' : patient.gender === 'FEMALE' ? 'Female' : patient.gender; return ( <>
{/* Header card */}
{/* Avatar + name */}

{fullName}

{patient.patientType && ( {patient.patientType === 'RETURNING' ? 'Returning' : 'New'} )} {age !== null && genderLabel && ( {age}y ยท {genderLabel} )} {leadInfo?.source && ( {leadInfo.source.replace(/_/g, ' ')} )} {leadInfo?.status && ( {leadInfo.status.replace(/_/g, ' ')} )}
{/* Contact details */}
{phoneRaw && ( {phoneRaw} )} {email && ( {email} )}
{leadInfo?.interestedService && ( Interested in: {leadInfo.interestedService} )}
{/* AI summary from linked lead */} {leadInfo?.aiSummary && (

{leadInfo.aiSummary}

)} {/* Quick actions */}
{phoneRaw && ( )}
{/* Tabs */}
setActiveTab(String(key))}> {(item) => ( )} {/* Appointments tab */}
{appointments.length === 0 ? ( ) : (
{appointments.map((appt: any) => ( { setEditingAppt(a); setApptFormOpen(true); }} /> ))}
)}
{/* Calls tab */}
{patientCalls.length === 0 ? ( ) : (
{patientCalls.map((call: any) => ( ))}
)}
{/* Timeline tab */}
{activities.length === 0 ? ( ) : (
{activities.map((activity, idx) => ( ))}
)}
{/* Notes tab */}
{/* Add note form */}