import { useState } from "react"; import { formatDistanceToNow, parseISO } from "date-fns"; import { useQuery, useMutation } from "@tanstack/react-query"; import { Sidebar } from "@/components/layout/sidebar"; import { TopAppBar } from "@/components/layout/top-app-bar"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Switch } from "@/components/ui/switch"; import { Phone, PhoneCall, Clock, User, Search, MessageSquare, Send, X, MoonStar, } from "lucide-react"; import { SmsTemplateDialog } from "@/components/patient-connection/sms-template-diaog"; import { MessageThread } from "@/components/patient-connection/message-thread"; import { useToast } from "@/hooks/use-toast"; import { apiRequest } from "@/lib/queryClient"; import type { Patient } from "@repo/db/types"; export default function PatientConnectionPage() { const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [searchTerm, setSearchTerm] = useState(""); const [selectedPatient, setSelectedPatient] = useState(null); const [isSmsDialogOpen, setIsSmsDialogOpen] = useState(false); const [showMessaging, setShowMessaging] = useState(false); const [afterHoursEnabled, setAfterHoursEnabled] = useState(true); const { toast } = useToast(); useQuery<{ enabled: boolean }>({ queryKey: ["/api/twilio/after-hours-handoff"], queryFn: async () => { const res = await apiRequest("GET", "/api/twilio/after-hours-handoff"); return res.json(); }, onSuccess: (data: { enabled: boolean }) => setAfterHoursEnabled(data.enabled), } as any); const afterHoursMutation = useMutation({ mutationFn: async (enabled: boolean) => apiRequest("PUT", "/api/twilio/after-hours-handoff", { enabled }), onSuccess: (_: any, enabled: boolean) => { toast({ title: enabled ? "After-hours AI enabled" : "After-hours AI disabled", description: enabled ? "AI will automatically handle messages outside office hours." : "After-hours messages will not receive an automatic AI reply.", }); }, }); const handleAfterHoursToggle = (enabled: boolean) => { setAfterHoursEnabled(enabled); afterHoursMutation.mutate(enabled); }; const toggleMobileMenu = () => { setIsMobileMenuOpen(!isMobileMenuOpen); }; const makeCallMutation = useMutation({ mutationFn: async ({ to, patientId, }: { to: string; patientId: number; }) => { return apiRequest("POST", "/api/twilio/make-call", { to, message: "Hello, this is a call from your dental office. We are calling to connect with you.", patientId, }); }, onSuccess: () => { toast({ title: "Call Initiated", description: "The call has been placed successfully.", }); }, onError: (error: any) => { toast({ title: "Call Failed", description: error.message || "Unable to place the call. Please try again.", variant: "destructive", }); }, }); // Fetch all patients from database const { data: patients = [], isLoading } = useQuery({ queryKey: ["/api/patients"], }); // Filter patients based on search term const filteredPatients = patients.filter((patient) => { if (!searchTerm) return false; const searchLower = searchTerm.toLowerCase(); const fullName = `${patient.firstName} ${patient.lastName}`.toLowerCase(); const phone = patient.phone?.toLowerCase() || ""; const patientId = patient.id?.toString(); return ( fullName.includes(searchLower) || phone.includes(searchLower) || patientId?.includes(searchLower) ); }); // Handle calling patient via Twilio const handleCall = (patient: Patient) => { if (!patient.phone?.trim()) { toast({ title: "No Phone Number", description: "This patient does not have a phone number on file.", variant: "destructive", }); return; } makeCallMutation.mutate({ to: patient.phone, patientId: Number(patient.id), }); }; // Handle sending SMS const handleSMS = (patient: Patient) => { setSelectedPatient(patient); setIsSmsDialogOpen(true); }; // Handle opening messaging const handleOpenMessaging = (patient: Patient) => { setSelectedPatient(patient); setShowMessaging(true); }; // Handle closing messaging const handleCloseMessaging = () => { setShowMessaging(false); setSelectedPatient(null); }; const { data: recentCommunications = [] } = useQuery({ queryKey: ["/api/twilio/recent-communications"], refetchInterval: 10000, }); // One entry per patient, keeping the most recent communication const recentPatients = Object.values( recentCommunications.reduce((acc: Record, comm: any) => { if (!comm.patient) return acc; if (!acc[comm.patient.id]) acc[comm.patient.id] = comm; return acc; }, {}) ); const callStats = [ { label: "Total Calls Today", value: "23", icon: , }, { label: "Answered Calls", value: "21", icon: , }, { label: "Average Call Time", value: "4:32", icon: , }, { label: "Active Patients", value: patients.length.toString(), icon: , }, ]; return (

Patient Connection

Search and communicate with patients

{/* After-hours AI toggle */}
Hand off to AI after hours
{/* Call Statistics */}
{callStats.map((stat, index) => (

{stat.label}

{stat.value}

{stat.icon}
))}
{/* Recent Calls */} Recent Conversations View and manage recent patient conversations
{recentPatients.length === 0 ? (

No conversations yet.

) : ( recentPatients.map((comm: any) => (
handleOpenMessaging(comm.patient)} >
{comm.patient.firstName?.[0]}{comm.patient.lastName?.[0]}

{comm.patient.firstName} {comm.patient.lastName}

{comm.createdAt ? formatDistanceToNow(parseISO(comm.createdAt), { addSuffix: true }) : ""}

)) )}
{/* Search and Actions */} Search Patients Search by name, phone number, or patient ID
setSearchTerm(e.target.value)} data-testid="input-patient-search" />
{/* Search Results */} {searchTerm && (
{isLoading ? (

Loading patients...

) : filteredPatients.length > 0 ? (

Search Results ({filteredPatients.length})

{filteredPatients.map((patient) => (

{patient.firstName} {patient.lastName}

{patient.phone || "No phone number"} • ID:{" "} {patient.id}

{patient.email && (

{patient.email}

)}
))}
) : (

No patients found matching "{searchTerm}"

)}
)}
{/* SMS Template Dialog */} {/* Messaging Interface */} {showMessaging && selectedPatient && (
)}
); }