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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<HTMLDivElement>(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
|
||||
</span>
|
||||
</SelectItem>
|
||||
{/* Reschedule patients — uses AI Reschedule Greeting */}
|
||||
<SelectItem value="__reschedule__" className="text-xs font-medium text-primary">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<CalendarX className="h-3 w-3" />
|
||||
Reschedule Patients
|
||||
</span>
|
||||
</SelectItem>
|
||||
<div className="my-1 border-t" />
|
||||
{templates.map((t) => (
|
||||
<SelectItem key={t.key} value={t.key} className="text-xs">
|
||||
@@ -469,6 +481,14 @@ export function MessageThread({ patient, onBack, appointmentInfo }: MessageThrea
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Reschedule flow indicator */}
|
||||
{pendingStartFlow === "reschedule" && (
|
||||
<div className="flex items-center gap-1 text-xs text-primary bg-primary/5 border border-primary/20 rounded px-2 py-0.5">
|
||||
<CalendarX className="h-3 w-3" />
|
||||
Reschedule flow
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* AI handoff toggle */}
|
||||
<div className="flex items-center gap-1.5 ml-auto">
|
||||
<Bot className={`h-3.5 w-3.5 flex-shrink-0 ${handOffToAI ? "text-primary" : "text-muted-foreground"}`} />
|
||||
|
||||
@@ -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<OfficeContact | null>({
|
||||
@@ -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: <CalendarX className="h-4 w-4 text-primary" />,
|
||||
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
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user