From 3ad5bd633d905c0281fb4e53342acf93ee04a9da Mon Sep 17 00:00:00 2001 From: Potenz Date: Thu, 18 Dec 2025 00:38:09 +0530 Subject: [PATCH] feat(patient-connection-page demo added) --- apps/Frontend/src/App.tsx | 2 + .../src/components/layout/sidebar.tsx | 6 + .../patient-connection/message-thread.tsx | 251 ++++++++++++ .../patient-connection/sms-template-diaog.tsx | 226 +++++++++++ .../src/pages/patient-connection-page.tsx | 380 ++++++++++++++++++ packages/db/prisma/schema.prisma | 87 +++- packages/db/types/index.ts | 3 +- packages/db/types/patientConnection-types.ts | 42 ++ packages/db/usedSchemas/index.ts | 3 +- 9 files changed, 977 insertions(+), 23 deletions(-) create mode 100644 apps/Frontend/src/components/patient-connection/message-thread.tsx create mode 100644 apps/Frontend/src/components/patient-connection/sms-template-diaog.tsx create mode 100644 apps/Frontend/src/pages/patient-connection-page.tsx create mode 100644 packages/db/types/patientConnection-types.ts diff --git a/apps/Frontend/src/App.tsx b/apps/Frontend/src/App.tsx index 1d1a49f..bea2914 100644 --- a/apps/Frontend/src/App.tsx +++ b/apps/Frontend/src/App.tsx @@ -12,6 +12,7 @@ import Dashboard from "./pages/dashboard"; import LoadingScreen from "./components/ui/LoadingScreen"; const AuthPage = lazy(() => import("./pages/auth-page")); +const PatientConnectionPage = lazy(() => import("./pages/patient-connection-page")); const AppointmentsPage = lazy(() => import("./pages/appointments-page")); const PatientsPage = lazy(() => import("./pages/patients-page")); const SettingsPage = lazy(() => import("./pages/settings-page")); @@ -34,6 +35,7 @@ function Router() { } /> } /> + } /> } diff --git a/apps/Frontend/src/components/layout/sidebar.tsx b/apps/Frontend/src/components/layout/sidebar.tsx index c526526..9f5b673 100644 --- a/apps/Frontend/src/components/layout/sidebar.tsx +++ b/apps/Frontend/src/components/layout/sidebar.tsx @@ -11,6 +11,7 @@ import { Database, FileText, Cloud, + Phone, } from "lucide-react"; import { cn } from "@/lib/utils"; import { useMemo } from "react"; @@ -27,6 +28,11 @@ export function Sidebar() { path: "/dashboard", icon: , }, + { + name: "Patient Connection", + path: "/patient-connection", + icon: , + }, { name: "Appointments", path: "/appointments", diff --git a/apps/Frontend/src/components/patient-connection/message-thread.tsx b/apps/Frontend/src/components/patient-connection/message-thread.tsx new file mode 100644 index 0000000..3d48ba4 --- /dev/null +++ b/apps/Frontend/src/components/patient-connection/message-thread.tsx @@ -0,0 +1,251 @@ +import { useState, useEffect, useRef } from "react"; +import { useQuery, useMutation } from "@tanstack/react-query"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { useToast } from "@/hooks/use-toast"; +import { apiRequest, queryClient } from "@/lib/queryClient"; +import { Send, ArrowLeft } from "lucide-react"; +import type { Patient, Communication } from "@repo/db/types"; +import { format, isToday, isYesterday, parseISO } from "date-fns"; + +interface MessageThreadProps { + patient: Patient; + onBack?: () => void; +} + +export function MessageThread({ patient, onBack }: MessageThreadProps) { + const { toast } = useToast(); + const [messageText, setMessageText] = useState(""); + const messagesEndRef = useRef(null); + + const { data: communications = [], isLoading } = useQuery({ + queryKey: ["/api/patients", patient.id, "communications"], + queryFn: async () => { + const res = await fetch(`/api/patients/${patient.id}/communications`, { + credentials: "include", + }); + if (!res.ok) throw new Error("Failed to fetch communications"); + return res.json(); + }, + refetchInterval: 5000, // Refresh every 5 seconds to get new messages + }); + + const sendMessageMutation = useMutation({ + mutationFn: async (message: string) => { + return apiRequest("POST", "/api/twilio/send-sms", { + to: patient.phone, + message: message, + patientId: patient.id, + }); + }, + onSuccess: () => { + setMessageText(""); + queryClient.invalidateQueries({ + queryKey: ["/api/patients", patient.id, "communications"], + }); + toast({ + title: "Message sent", + description: "Your message has been sent successfully.", + }); + }, + onError: (error: any) => { + toast({ + title: "Failed to send message", + description: + error.message || "Unable to send message. Please try again.", + variant: "destructive", + }); + }, + }); + + const handleSendMessage = () => { + if (!messageText.trim()) return; + sendMessageMutation.mutate(messageText); + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSendMessage(); + } + }; + + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [communications]); + + const formatMessageDate = (dateValue: string | Date) => { + const date = + typeof dateValue === "string" ? parseISO(dateValue) : dateValue; + if (isToday(date)) { + return format(date, "h:mm a"); + } else if (isYesterday(date)) { + return `Yesterday ${format(date, "h:mm a")}`; + } else { + return format(date, "MMM d, h:mm a"); + } + }; + + const getDateDivider = (dateValue: string | Date) => { + const messageDate = + typeof dateValue === "string" ? parseISO(dateValue) : dateValue; + if (isToday(messageDate)) { + return "Today"; + } else if (isYesterday(messageDate)) { + return "Yesterday"; + } else { + return format(messageDate, "MMMM d, yyyy"); + } + }; + + const groupedMessages: { date: string; messages: Communication[] }[] = []; + communications.forEach((comm) => { + if (!comm.createdAt) return; + const messageDate = + typeof comm.createdAt === "string" + ? parseISO(comm.createdAt) + : comm.createdAt; + const dateKey = format(messageDate, "yyyy-MM-dd"); + const existingGroup = groupedMessages.find((g) => g.date === dateKey); + if (existingGroup) { + existingGroup.messages.push(comm); + } else { + groupedMessages.push({ date: dateKey, messages: [comm] }); + } + }); + + return ( +
+ {/* Header */} +
+
+ {onBack && ( + + )} +
+
+ {patient.firstName[0]} + {patient.lastName[0]} +
+
+

+ {patient.firstName} {patient.lastName} +

+

{patient.phone}

+
+
+
+
+ + {/* Messages */} +
+ {isLoading ? ( +
+

Loading messages...

+
+ ) : communications.length === 0 ? ( +
+

+ No messages yet. Start the conversation! +

+
+ ) : ( + <> + {groupedMessages.map((group) => ( +
+ {/* Date Divider */} +
+
+ {getDateDivider(group.messages[0]?.createdAt!)} +
+
+ + {/* Messages for this date */} + {group.messages.map((comm) => ( +
+
+ {comm.direction === "inbound" && ( +
+
+ {patient.firstName[0]} + {patient.lastName[0]} +
+
+
+

+ {comm.body} +

+
+

+ {comm.createdAt && + formatMessageDate(comm.createdAt)} +

+
+
+ )} + + {comm.direction === "outbound" && ( +
+
+

+ {comm.body} +

+
+

+ {comm.createdAt && + formatMessageDate(comm.createdAt)} +

+
+ )} +
+
+ ))} +
+ ))} +
+ + )} +
+ + {/* Input Area */} +
+
+ setMessageText(e.target.value)} + onKeyPress={handleKeyPress} + placeholder="Type your message..." + className="flex-1 rounded-full" + disabled={sendMessageMutation.isPending} + data-testid="input-message" + /> + +
+
+
+ ); +} diff --git a/apps/Frontend/src/components/patient-connection/sms-template-diaog.tsx b/apps/Frontend/src/components/patient-connection/sms-template-diaog.tsx new file mode 100644 index 0000000..3f74254 --- /dev/null +++ b/apps/Frontend/src/components/patient-connection/sms-template-diaog.tsx @@ -0,0 +1,226 @@ +import { useState } from "react"; +import { useMutation } from "@tanstack/react-query"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; +import { Label } from "@/components/ui/label"; +import { MessageSquare, Send, Loader2 } from "lucide-react"; +import { useToast } from "@/hooks/use-toast"; +import { apiRequest } from "@/lib/queryClient"; +import type { Patient } from "@repo/db/types"; + +interface SmsTemplateDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + patient: Patient | null; +} + +const MESSAGE_TEMPLATES = { + appointment_reminder: { + name: "Appointment Reminder", + template: (firstName: string) => + `Hi ${firstName}, this is your dental office. Reminder: You have an appointment scheduled. Please confirm or call us if you need to reschedule.`, + }, + appointment_confirmation: { + name: "Appointment Confirmation", + template: (firstName: string) => + `Hi ${firstName}, your appointment has been confirmed. We look forward to seeing you! If you have any questions, please call our office.`, + }, + follow_up: { + name: "Follow-Up", + template: (firstName: string) => + `Hi ${firstName}, thank you for visiting our dental office. How are you feeling after your treatment? Please let us know if you have any concerns.`, + }, + payment_reminder: { + name: "Payment Reminder", + template: (firstName: string) => + `Hi ${firstName}, this is a friendly reminder about your outstanding balance. Please contact our office to discuss payment options.`, + }, + general: { + name: "General Message", + template: (firstName: string) => + `Hi ${firstName}, this is your dental office. `, + }, + custom: { + name: "Custom Message", + template: () => "", + }, +}; + +export function SmsTemplateDialog({ + open, + onOpenChange, + patient, +}: SmsTemplateDialogProps) { + const [selectedTemplate, setSelectedTemplate] = useState< + keyof typeof MESSAGE_TEMPLATES + >("appointment_reminder"); + const [customMessage, setCustomMessage] = useState(""); + const { toast } = useToast(); + + const sendSmsMutation = useMutation({ + mutationFn: async ({ + to, + message, + patientId, + }: { + to: string; + message: string; + patientId: number; + }) => { + return apiRequest("POST", "/api/twilio/send-sms", { + to, + message, + patientId, + }); + }, + onSuccess: () => { + toast({ + title: "SMS Sent Successfully", + description: `Message sent to ${patient?.firstName} ${patient?.lastName}`, + }); + onOpenChange(false); + // Reset state + setSelectedTemplate("appointment_reminder"); + setCustomMessage(""); + }, + onError: (error: any) => { + toast({ + title: "Failed to Send SMS", + description: + error.message || + "Please check your Twilio configuration and try again.", + variant: "destructive", + }); + }, + }); + + const getMessage = () => { + if (!patient) return ""; + + if (selectedTemplate === "custom") { + return customMessage; + } + + return MESSAGE_TEMPLATES[selectedTemplate].template(patient.firstName); + }; + + const handleSend = () => { + if (!patient || !patient.phone) return; + + const message = getMessage(); + if (!message.trim()) return; + + sendSmsMutation.mutate({ + to: patient.phone, + message: message, + patientId: Number(patient.id), + }); + }; + + const handleTemplateChange = (value: string) => { + const templateKey = value as keyof typeof MESSAGE_TEMPLATES; + setSelectedTemplate(templateKey); + + // Pre-fill custom message if not custom template + if (templateKey !== "custom" && patient) { + setCustomMessage( + MESSAGE_TEMPLATES[templateKey].template(patient.firstName) + ); + } + }; + + return ( + + + + + + Send SMS to {patient?.firstName} {patient?.lastName} + + + Choose a message template or write a custom message + + + +
+
+ + +
+ +
+ +