From e9296c68f92fddcad6c957751de12eb2589ae5cb Mon Sep 17 00:00:00 2001 From: Gitead Date: Fri, 8 May 2026 14:30:29 -0400 Subject: [PATCH] feat: auto-populate patient fields from member ID on eligibility page When a member ID is typed on the insurance eligibility page, debounced lookup fills in date of birth, first name, and last name if the patient already exists in the database. Co-Authored-By: Claude Sonnet 4.6 --- apps/Backend/src/ai/aiHandoffStore.ts | 6 +++++ apps/Backend/src/routes/ai-settings.ts | 4 +-- apps/Backend/src/routes/twilio.ts | 4 ++- apps/Backend/src/storage/twilio-storage.ts | 4 ++- .../patient-connection/message-thread.tsx | 26 +++++++++++++++--- .../settings/ai-chat-templates-card.tsx | 18 ++++++++++++- .../src/pages/insurance-status-page.tsx | 27 +++++++++++++++++++ 7 files changed, 81 insertions(+), 8 deletions(-) diff --git a/apps/Backend/src/ai/aiHandoffStore.ts b/apps/Backend/src/ai/aiHandoffStore.ts index 33e4e961..58dc5b78 100644 --- a/apps/Backend/src/ai/aiHandoffStore.ts +++ b/apps/Backend/src/ai/aiHandoffStore.ts @@ -72,6 +72,12 @@ export function startNewPatientConversation(userId: number, patientId: number): stageStore.set(convKey(userId, patientId), "new_patient_greeted"); } +// Called when office sends a reschedule greeting — patient's next reply enters +// the reschedule flow. +export function startRescheduleConversation(userId: number, patientId: number): void { + stageStore.set(convKey(userId, patientId), "asked_reschedule_confirm"); +} + // ── Pending reschedule data ─────────────────────────────────────────────────── // Holds the confirmed new date while AI waits for a time-slot answer. diff --git a/apps/Backend/src/routes/ai-settings.ts b/apps/Backend/src/routes/ai-settings.ts index 0792109d..87b42e33 100644 --- a/apps/Backend/src/routes/ai-settings.ts +++ b/apps/Backend/src/routes/ai-settings.ts @@ -53,8 +53,8 @@ router.put("/chat-templates", async (req: Request, res: Response): Promise try { const userId = req.user?.id; if (!userId) return res.status(401).json({ message: "Unauthorized" }); - const { reminderGreeting, newPatientGreeting, generalFallback } = req.body; - await storage.saveAiChatTemplates(userId, { reminderGreeting, newPatientGreeting, generalFallback }); + const { reminderGreeting, newPatientGreeting, generalFallback, rescheduleGreeting } = req.body; + await storage.saveAiChatTemplates(userId, { reminderGreeting, newPatientGreeting, generalFallback, rescheduleGreeting }); const updated = await storage.getAiChatTemplates(userId); return res.status(200).json(updated); } catch (err) { diff --git a/apps/Backend/src/routes/twilio.ts b/apps/Backend/src/routes/twilio.ts index 3529ae46..adaa4756 100644 --- a/apps/Backend/src/routes/twilio.ts +++ b/apps/Backend/src/routes/twilio.ts @@ -1,7 +1,7 @@ import express, { Request, Response } from "express"; import twilio from "twilio"; import { storage } from "../storage"; -import { getHandoff, setHandoff, resetConversation, startNewPatientConversation, getAfterHoursHandoff, setAfterHoursHandoff } from "../ai/aiHandoffStore"; +import { getHandoff, setHandoff, resetConversation, startNewPatientConversation, startRescheduleConversation, getAfterHoursHandoff, setAfterHoursHandoff } from "../ai/aiHandoffStore"; const router = express.Router(); @@ -95,6 +95,8 @@ router.post("/send-sms", async (req: Request, res: Response): Promise => { // Set conversation stage based on which flow was started if (startFlow === "new_patient") { startNewPatientConversation(userId, pid); + } else if (startFlow === "reschedule") { + startRescheduleConversation(userId, pid); } else { resetConversation(userId, pid); } diff --git a/apps/Backend/src/storage/twilio-storage.ts b/apps/Backend/src/storage/twilio-storage.ts index 7a814d99..7d57ad7d 100644 --- a/apps/Backend/src/storage/twilio-storage.ts +++ b/apps/Backend/src/storage/twilio-storage.ts @@ -66,16 +66,18 @@ export const twilioStorage = { reminderGreeting: all["_ai_chat_reminder_greeting"] ?? "", newPatientGreeting: all["_ai_chat_new_patient_greeting"] ?? "", generalFallback: all["_ai_chat_general_fallback"] ?? "", + rescheduleGreeting: all["_ai_chat_reschedule_greeting"] ?? "", }; }, - async saveAiChatTemplates(userId: number, templates: { reminderGreeting?: string; newPatientGreeting?: string; generalFallback?: string }) { + async saveAiChatTemplates(userId: number, templates: { reminderGreeting?: string; newPatientGreeting?: string; generalFallback?: string; rescheduleGreeting?: string }) { const settings = await db.twilioSettings.findUnique({ where: { userId } }); const existing = (settings?.templates as Record) || {}; const updated: Record = { ...existing }; if (templates.reminderGreeting !== undefined) updated["_ai_chat_reminder_greeting"] = templates.reminderGreeting; if (templates.newPatientGreeting !== undefined) updated["_ai_chat_new_patient_greeting"] = templates.newPatientGreeting; if (templates.generalFallback !== undefined) updated["_ai_chat_general_fallback"] = templates.generalFallback; + if (templates.rescheduleGreeting !== undefined) updated["_ai_chat_reschedule_greeting"] = templates.rescheduleGreeting; return db.twilioSettings.upsert({ where: { userId }, update: { templates: updated }, diff --git a/apps/Frontend/src/components/patient-connection/message-thread.tsx b/apps/Frontend/src/components/patient-connection/message-thread.tsx index c9f3faea..fea971c8 100755 --- a/apps/Frontend/src/components/patient-connection/message-thread.tsx +++ b/apps/Frontend/src/components/patient-connection/message-thread.tsx @@ -11,7 +11,7 @@ import { } from "@/components/ui/select"; import { useToast } from "@/hooks/use-toast"; import { apiRequest, queryClient } from "@/lib/queryClient"; -import { Send, ArrowLeft, FileText, Globe, Bot, UserPlus } from "lucide-react"; +import { Send, ArrowLeft, FileText, Globe, Bot, UserPlus, CalendarX } from "lucide-react"; import { Switch } from "@/components/ui/switch"; import type { Patient, Communication } from "@repo/db/types"; import { format, isToday, isYesterday, parseISO } from "date-fns"; @@ -266,7 +266,7 @@ export function MessageThread({ patient, onBack, appointmentInfo }: MessageThrea ); const messagesEndRef = useRef(null); const [handOffToAI, setHandOffToAI] = useState(true); - const [pendingStartFlow, setPendingStartFlow] = useState<"new_patient" | null>(null); + const [pendingStartFlow, setPendingStartFlow] = useState<"new_patient" | "reschedule" | null>(null); useQuery<{ enabled: boolean }>({ queryKey: ["/api/twilio/ai-handoff", patient.id], @@ -277,7 +277,7 @@ export function MessageThread({ patient, onBack, appointmentInfo }: MessageThrea onSuccess: (data: { enabled: boolean }) => setHandOffToAI(data.enabled), } as any); - const { data: aiChatTemplates } = useQuery<{ newPatientGreeting: string } | null>({ + const { data: aiChatTemplates } = useQuery<{ newPatientGreeting: string; rescheduleGreeting: string } | null>({ queryKey: ["/api/ai/chat-templates"], queryFn: async () => { const res = await apiRequest("GET", "/api/ai/chat-templates"); @@ -434,6 +434,11 @@ export function MessageThread({ patient, onBack, appointmentInfo }: MessageThrea "Hi! My name is Lisa, the dedicated AI assistant at our dental office. I can help you schedule an appointment, check your insurance, and answer general questions 24/7. How can I help you today?"; setMessageText(greeting); setPendingStartFlow("new_patient"); + } else if (key === "__reschedule__") { + const greeting = aiChatTemplates?.rescheduleGreeting || + "Hi! My name is Lisa, the dedicated AI assistant at our dental office. I can help you find a new appointment time that works for you. Would you like to reschedule your appointment?"; + setMessageText(greeting); + setPendingStartFlow("reschedule"); } else { const tpl = templates.find((t) => t.key === key); if (tpl) { setMessageText(tpl.body); setPendingStartFlow(null); } @@ -451,6 +456,13 @@ export function MessageThread({ patient, onBack, appointmentInfo }: MessageThrea Schedule a New Patient + {/* Reschedule patients — uses AI Reschedule Greeting */} + + + + Reschedule Patients + +
{templates.map((t) => ( @@ -469,6 +481,14 @@ export function MessageThread({ patient, onBack, appointmentInfo }: MessageThrea
)} + {/* Reschedule flow indicator */} + {pendingStartFlow === "reschedule" && ( +
+ + Reschedule flow +
+ )} + {/* AI handoff toggle */}
diff --git a/apps/Frontend/src/components/settings/ai-chat-templates-card.tsx b/apps/Frontend/src/components/settings/ai-chat-templates-card.tsx index 49e16383..ab6cbb46 100644 --- a/apps/Frontend/src/components/settings/ai-chat-templates-card.tsx +++ b/apps/Frontend/src/components/settings/ai-chat-templates-card.tsx @@ -5,12 +5,13 @@ import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { useToast } from "@/hooks/use-toast"; import { apiRequest, queryClient } from "@/lib/queryClient"; -import { Bot, CalendarCheck, UserPlus, MessageCircle, Info } from "lucide-react"; +import { Bot, CalendarCheck, UserPlus, MessageCircle, Info, CalendarX } from "lucide-react"; type AiChatTemplates = { reminderGreeting: string; newPatientGreeting: string; generalFallback: string; + rescheduleGreeting: string; }; type OfficeContact = { @@ -24,6 +25,8 @@ const DEFAULTS = { "Hi! My name is Lisa, the dedicated AI assistant at {officeName}. I can help you schedule an appointment, check your insurance, and answer general questions 24/7. How can I help you today?", generalFallback: "How can I help you today?", + rescheduleGreeting: + "Hi! My name is Lisa, the dedicated AI assistant at {officeName}. I can help you find a new appointment time that works for you. Would you like to reschedule your appointment?", }; function preview(text: string, officeName: string) { @@ -36,6 +39,7 @@ export function AiChatTemplatesCard() { const [reminderGreeting, setReminderGreeting] = useState(DEFAULTS.reminderGreeting); const [newPatientGreeting, setNewPatientGreeting] = useState(DEFAULTS.newPatientGreeting); const [generalFallback, setGeneralFallback] = useState(DEFAULTS.generalFallback); + const [rescheduleGreeting, setRescheduleGreeting] = useState(DEFAULTS.rescheduleGreeting); const initialized = useRef(false); const { data: officeContact } = useQuery({ @@ -66,6 +70,7 @@ export function AiChatTemplatesCard() { setReminderGreeting(templates.reminderGreeting || DEFAULTS.reminderGreeting); setNewPatientGreeting(templates.newPatientGreeting || DEFAULTS.newPatientGreeting); setGeneralFallback(templates.generalFallback || DEFAULTS.generalFallback); + setRescheduleGreeting(templates.rescheduleGreeting || DEFAULTS.rescheduleGreeting); } }, [templates]); @@ -93,6 +98,7 @@ export function AiChatTemplatesCard() { reminderGreeting: reminderGreeting.trim() || DEFAULTS.reminderGreeting, newPatientGreeting: newPatientGreeting.trim() || DEFAULTS.newPatientGreeting, generalFallback: generalFallback.trim() || DEFAULTS.generalFallback, + rescheduleGreeting: rescheduleGreeting.trim() || DEFAULTS.rescheduleGreeting, }); }; @@ -126,6 +132,15 @@ export function AiChatTemplatesCard() { onChange: setGeneralFallback, placeholder: DEFAULTS.generalFallback, }, + { + key: "reschedule" as const, + icon: , + label: "Reschedule Patients", + description: "Sent when the office initiates a reschedule flow for a patient.", + value: rescheduleGreeting, + onChange: setRescheduleGreeting, + placeholder: DEFAULTS.rescheduleGreeting, + }, ]; return ( @@ -196,6 +211,7 @@ export function AiChatTemplatesCard() { setReminderGreeting(DEFAULTS.reminderGreeting); setNewPatientGreeting(DEFAULTS.newPatientGreeting); setGeneralFallback(DEFAULTS.generalFallback); + setRescheduleGreeting(DEFAULTS.rescheduleGreeting); }} > Reset to defaults diff --git a/apps/Frontend/src/pages/insurance-status-page.tsx b/apps/Frontend/src/pages/insurance-status-page.tsx index 048f4fb2..0d7eb9d1 100755 --- a/apps/Frontend/src/pages/insurance-status-page.tsx +++ b/apps/Frontend/src/pages/insurance-status-page.tsx @@ -110,6 +110,33 @@ export default function InsuranceStatusPage() { } }, [selectedPatient]); + // Auto-lookup patient by member ID when typed manually + useEffect(() => { + if (selectedPatient && memberId === (selectedPatient.insuranceId ?? "")) return; + if (!memberId) return; + + const timer = setTimeout(async () => { + try { + const res = await apiRequest("GET", `/api/patients/by-insurance-id?insuranceId=${encodeURIComponent(memberId)}`); + if (!res.ok) return; + const patient: Patient | null = await res.json(); + if (patient) { + setFirstName(patient.firstName ?? ""); + setLastName(patient.lastName ?? ""); + const dob = + typeof patient.dateOfBirth === "string" + ? parseLocalDate(patient.dateOfBirth) + : patient.dateOfBirth ?? null; + setDateOfBirth(dob); + } + } catch { + // silently ignore lookup errors + } + }, 500); + + return () => clearTimeout(timer); + }, [memberId, selectedPatient]); + // Add patient mutation const addPatientMutation = useMutation({ mutationFn: async (patient: InsertPatient) => {