feat: AI chat scheduling, claim automation, and session improvements
- Internal AI chat: schedule_appointment intent books earliest available slot in Column A using office hours; claim_only intent looks up latest past appointment for service date, asks user when two appointments are within 7 days, auto-triggers correct Selenium worker with mapped prices - Gemini model updated to gemini-flash-latest; conversation history (15 messages) passed for pronoun/reference resolution; history trimmed to start with user turn so Gemini doesn't reject the context - Insurance alias file (insuranceAliases.json) replaces hardcoded siteKey matching; "tufs" now resolves to TUFTS_SCO - DOB format normalized (MM/DD/YYYY → YYYY-MM-DD) before parseLocalDate; autoCheck now fires for all insurance types, not just MH/CMSP - Claim form auto-submit: all handlers (MH, CCA, DDMA, UnitedDH, Tufts) accept formToUse and receive fee-schedule-priced form; prefillDone set after chatbot code prefill so autoSubmit gate opens correctly - Chatbot: chat history persisted in sessionStorage, cleared on logout and auto-logout; Clear button writes fresh state synchronously; message history window increased to 15 - DentaQuest/TuftsSCO Selenium: "Remember me" checkbox clicked before sign-in to persist OTP trust cookie across sessions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -79,6 +79,8 @@ interface ClaimFormProps {
|
||||
patientId: number;
|
||||
appointmentId?: number;
|
||||
autoSubmit?: boolean;
|
||||
/** When set, autoSubmit triggers this insurance's Selenium worker instead of MH */
|
||||
autoSubmitSiteKey?: string;
|
||||
/** When true: form saves to AppointmentProcedure (Select Procedures flow), shows only Save button */
|
||||
proceduresOnly?: boolean;
|
||||
onSubmit: (data: ClaimFormData) => Promise<Claim>;
|
||||
@@ -101,6 +103,7 @@ export function ClaimForm({
|
||||
patientId,
|
||||
appointmentId,
|
||||
autoSubmit,
|
||||
autoSubmitSiteKey,
|
||||
proceduresOnly = false,
|
||||
onHandleAppointmentSubmit,
|
||||
onHandleUpdatePatient,
|
||||
@@ -487,6 +490,41 @@ export function ClaimForm({
|
||||
};
|
||||
}, [appointmentId, serviceDate, existingClaimId]);
|
||||
|
||||
// Prefill service lines (and optional service date) from chatbot claim_only flow
|
||||
useEffect(() => {
|
||||
const raw = sessionStorage.getItem("chatbot_claim_prefill");
|
||||
if (!raw) return;
|
||||
try {
|
||||
const { codes, serviceDate } = JSON.parse(raw) as {
|
||||
codes: { code: string; description: string }[];
|
||||
serviceDate?: string;
|
||||
};
|
||||
sessionStorage.removeItem("chatbot_claim_prefill");
|
||||
if (!codes?.length) return;
|
||||
|
||||
if (serviceDate) {
|
||||
try {
|
||||
const d = parseLocalDate(serviceDate);
|
||||
setServiceDateValue(d);
|
||||
setServiceDate(formatLocalDate(d));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
setForm((prev) => {
|
||||
const date = serviceDate ? serviceDate : prev.serviceDate;
|
||||
const updatedLines = [...prev.serviceLines];
|
||||
codes.forEach((c, i) => {
|
||||
if (i < updatedLines.length) {
|
||||
updatedLines[i] = { ...updatedLines[i]!, procedureCode: c.code, procedureDate: date };
|
||||
}
|
||||
});
|
||||
return { ...prev, serviceLines: updatedLines };
|
||||
});
|
||||
|
||||
if (!appointmentId) setPrefillDone(true);
|
||||
} catch {}
|
||||
}, []);
|
||||
|
||||
// Restore NPI provider from saved procedures when npiProviders list loads after 2b
|
||||
useEffect(() => {
|
||||
if (!savedProcNpiId || !npiProviders.length) return;
|
||||
@@ -949,10 +987,11 @@ export function ClaimForm({
|
||||
};
|
||||
|
||||
// 3rd Button workflow — CCA Claim: saves to DB then submits via Selenium
|
||||
const handleCCAClaim = async () => {
|
||||
const handleCCAClaim = async (formToUse?: ClaimFormData & { uploadedFiles?: File[] }) => {
|
||||
const f = formToUse ?? form;
|
||||
const missingFields: string[] = [];
|
||||
if (!form.memberId?.trim()) missingFields.push("Member ID");
|
||||
if (!form.dateOfBirth?.trim()) missingFields.push("Date of Birth");
|
||||
if (!f.memberId?.trim()) missingFields.push("Member ID");
|
||||
if (!f.dateOfBirth?.trim()) missingFields.push("Date of Birth");
|
||||
if (!patient?.firstName?.trim()) missingFields.push("First Name");
|
||||
if (missingFields.length > 0) {
|
||||
toast({
|
||||
@@ -963,7 +1002,7 @@ export function ClaimForm({
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredServiceLines = (form.serviceLines || []).filter(
|
||||
const filteredServiceLines = (f.serviceLines || []).filter(
|
||||
(line) => (line.procedureCode ?? "").trim() !== "",
|
||||
);
|
||||
if (filteredServiceLines.length === 0) {
|
||||
@@ -990,10 +1029,10 @@ export function ClaimForm({
|
||||
}
|
||||
}
|
||||
|
||||
const { uploadedFiles, insuranceSiteKey, npiProvider, ...formToCreateClaim } = form;
|
||||
const claimFilesMeta: ClaimFileMeta[] = (uploadedFiles || []).map((f) => ({
|
||||
filename: f.name,
|
||||
mimeType: f.type,
|
||||
const { uploadedFiles, insuranceSiteKey, npiProvider, ...formToCreateClaim } = f;
|
||||
const claimFilesMeta: ClaimFileMeta[] = (uploadedFiles || []).map((file) => ({
|
||||
filename: file.name,
|
||||
mimeType: file.type,
|
||||
}));
|
||||
|
||||
const selectedNpiProviderId = npiProvider?.npiNumber
|
||||
@@ -1014,7 +1053,7 @@ export function ClaimForm({
|
||||
|
||||
// Send to CCA Selenium — send raw YYYY-MM-DD so Python _format_dob converts correctly
|
||||
onHandleForCCASeleniumClaim({
|
||||
...form,
|
||||
...f,
|
||||
serviceLines: filteredServiceLines,
|
||||
staffId: appointmentStaffId ?? Number(staff?.id),
|
||||
patientId,
|
||||
@@ -1028,10 +1067,11 @@ export function ClaimForm({
|
||||
};
|
||||
|
||||
// Delta MA Claim: saves to DB then submits via Selenium
|
||||
const handleDDMAClaim = async () => {
|
||||
const handleDDMAClaim = async (formToUse?: ClaimFormData & { uploadedFiles?: File[] }) => {
|
||||
const f = formToUse ?? form;
|
||||
const missingFields: string[] = [];
|
||||
if (!form.memberId?.trim()) missingFields.push("Member ID");
|
||||
if (!form.dateOfBirth?.trim()) missingFields.push("Date of Birth");
|
||||
if (!f.memberId?.trim()) missingFields.push("Member ID");
|
||||
if (!f.dateOfBirth?.trim()) missingFields.push("Date of Birth");
|
||||
if (!patient?.firstName?.trim()) missingFields.push("First Name");
|
||||
if (missingFields.length > 0) {
|
||||
toast({
|
||||
@@ -1042,7 +1082,7 @@ export function ClaimForm({
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredServiceLines = (form.serviceLines || []).filter(
|
||||
const filteredServiceLines = (f.serviceLines || []).filter(
|
||||
(line) => (line.procedureCode ?? "").trim() !== "",
|
||||
);
|
||||
if (filteredServiceLines.length === 0) {
|
||||
@@ -1068,7 +1108,7 @@ export function ClaimForm({
|
||||
}
|
||||
}
|
||||
|
||||
const { uploadedFiles, insuranceSiteKey, npiProvider, ...formToCreateClaim } = form;
|
||||
const { uploadedFiles, insuranceSiteKey, npiProvider, ...formToCreateClaim } = f;
|
||||
|
||||
// Upload files to server so we get local filePaths for Selenium
|
||||
const claimFilesMeta: ClaimFileMeta[] = uploadedFiles?.length
|
||||
@@ -1091,7 +1131,7 @@ export function ClaimForm({
|
||||
});
|
||||
|
||||
onHandleForDDMASeleniumClaim({
|
||||
...form,
|
||||
...f,
|
||||
serviceLines: filteredServiceLines,
|
||||
staffId: appointmentStaffId ?? Number(staff?.id),
|
||||
patientId,
|
||||
@@ -1106,10 +1146,11 @@ export function ClaimForm({
|
||||
};
|
||||
|
||||
// United/DentalHub Claim: saves to DB then submits via Selenium
|
||||
const handleUnitedDHClaim = async () => {
|
||||
const handleUnitedDHClaim = async (formToUse?: ClaimFormData & { uploadedFiles?: File[] }) => {
|
||||
const f = formToUse ?? form;
|
||||
const missingFields: string[] = [];
|
||||
if (!form.memberId?.trim()) missingFields.push("Member ID");
|
||||
if (!form.dateOfBirth?.trim()) missingFields.push("Date of Birth");
|
||||
if (!f.memberId?.trim()) missingFields.push("Member ID");
|
||||
if (!f.dateOfBirth?.trim()) missingFields.push("Date of Birth");
|
||||
if (!patient?.firstName?.trim()) missingFields.push("First Name");
|
||||
if (missingFields.length > 0) {
|
||||
toast({
|
||||
@@ -1120,7 +1161,7 @@ export function ClaimForm({
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredServiceLines = (form.serviceLines || []).filter(
|
||||
const filteredServiceLines = (f.serviceLines || []).filter(
|
||||
(line) => (line.procedureCode ?? "").trim() !== "",
|
||||
);
|
||||
if (filteredServiceLines.length === 0) {
|
||||
@@ -1146,7 +1187,7 @@ export function ClaimForm({
|
||||
}
|
||||
}
|
||||
|
||||
const { uploadedFiles, insuranceSiteKey, npiProvider, ...formToCreateClaim } = form;
|
||||
const { uploadedFiles, insuranceSiteKey, npiProvider, ...formToCreateClaim } = f;
|
||||
|
||||
const claimFilesMeta: ClaimFileMeta[] = uploadedFiles?.length
|
||||
? await uploadAttachmentsToLocalFolder(uploadedFiles)
|
||||
@@ -1168,7 +1209,7 @@ export function ClaimForm({
|
||||
});
|
||||
|
||||
onHandleForUnitedDHSeleniumClaim({
|
||||
...form,
|
||||
...f,
|
||||
serviceLines: filteredServiceLines,
|
||||
staffId: appointmentStaffId ?? Number(staff?.id),
|
||||
patientId,
|
||||
@@ -1183,10 +1224,11 @@ export function ClaimForm({
|
||||
};
|
||||
|
||||
// Tufts SCO Claim: saves to DB then submits via Selenium
|
||||
const handleTuftsSCOClaim = async () => {
|
||||
const handleTuftsSCOClaim = async (formToUse?: ClaimFormData & { uploadedFiles?: File[] }) => {
|
||||
const f = formToUse ?? form;
|
||||
const missingFields: string[] = [];
|
||||
if (!form.memberId?.trim()) missingFields.push("Member ID");
|
||||
if (!form.dateOfBirth?.trim()) missingFields.push("Date of Birth");
|
||||
if (!f.memberId?.trim()) missingFields.push("Member ID");
|
||||
if (!f.dateOfBirth?.trim()) missingFields.push("Date of Birth");
|
||||
if (!patient?.firstName?.trim()) missingFields.push("First Name");
|
||||
if (missingFields.length > 0) {
|
||||
toast({
|
||||
@@ -1197,7 +1239,7 @@ export function ClaimForm({
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredServiceLines = (form.serviceLines || []).filter(
|
||||
const filteredServiceLines = (f.serviceLines || []).filter(
|
||||
(line) => (line.procedureCode ?? "").trim() !== "",
|
||||
);
|
||||
if (filteredServiceLines.length === 0) {
|
||||
@@ -1223,7 +1265,7 @@ export function ClaimForm({
|
||||
}
|
||||
}
|
||||
|
||||
const { uploadedFiles, insuranceSiteKey, npiProvider, ...formToCreateClaim } = form;
|
||||
const { uploadedFiles, insuranceSiteKey, npiProvider, ...formToCreateClaim } = f;
|
||||
|
||||
const claimFilesMeta: ClaimFileMeta[] = uploadedFiles?.length
|
||||
? await uploadAttachmentsToLocalFolder(uploadedFiles)
|
||||
@@ -1255,7 +1297,7 @@ export function ClaimForm({
|
||||
}
|
||||
|
||||
onHandleForTuftsSCOSeleniumClaim({
|
||||
...form,
|
||||
...f,
|
||||
serviceLines: filteredServiceLines,
|
||||
staffId: appointmentStaffId ?? Number(staff?.id),
|
||||
patientId,
|
||||
@@ -1656,8 +1698,30 @@ export function ClaimForm({
|
||||
if (autoSubmittedRef.current) return;
|
||||
autoSubmittedRef.current = true;
|
||||
|
||||
handleMHSubmit();
|
||||
}, [autoSubmit, prefillDone, isFormReady]);
|
||||
// Apply fee-schedule prices before triggering so billed amounts are populated
|
||||
const siteKeyForPricing = autoSubmitSiteKey
|
||||
? autoSubmitSiteKey.replace(/_/g, "").toLowerCase()
|
||||
: deriveInsuranceSiteKey(form.insuranceProvider || "");
|
||||
|
||||
const pricedForm = mapPricesForForm({
|
||||
form: { ...form, insuranceSiteKey: siteKeyForPricing },
|
||||
patientDOB: patient?.dateOfBirth ?? "",
|
||||
insuranceSiteKey: siteKeyForPricing,
|
||||
});
|
||||
|
||||
const key = (autoSubmitSiteKey ?? "").toLowerCase();
|
||||
if (key === "tufts_sco" || key === "tuftsco" || key === "tufts sco") {
|
||||
handleTuftsSCOClaim(pricedForm);
|
||||
} else if (key === "cca") {
|
||||
handleCCAClaim(pricedForm);
|
||||
} else if (key === "ddma") {
|
||||
handleDDMAClaim(pricedForm);
|
||||
} else if (key === "united_sco" || key === "unitedco" || key === "dentalhub") {
|
||||
handleUnitedDHClaim(pricedForm);
|
||||
} else {
|
||||
handleMHSubmit(pricedForm);
|
||||
}
|
||||
}, [autoSubmit, autoSubmitSiteKey, prefillDone, isFormReady]);
|
||||
|
||||
// overlay click handler (close when clicking backdrop)
|
||||
const onOverlayMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
MessageSquare,
|
||||
Send,
|
||||
Loader2,
|
||||
RotateCcw,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -24,7 +25,8 @@ type Step =
|
||||
| "patient-found"
|
||||
| "eligibility-id-ready"
|
||||
| "check-and-claim-ready"
|
||||
| "need-insurance-clarification";
|
||||
| "need-insurance-clarification"
|
||||
| "need-appointment-selection";
|
||||
|
||||
interface Message {
|
||||
id: number;
|
||||
@@ -96,14 +98,20 @@ function parseEligibilityInput(
|
||||
};
|
||||
}
|
||||
|
||||
const INITIAL_MESSAGES: Message[] = [
|
||||
makeMsg("bot", "Hi! What can I help you with today?"),
|
||||
];
|
||||
const CHAT_STORAGE_KEY = "chatbot_messages";
|
||||
|
||||
function loadSavedMessages(): Message[] {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(CHAT_STORAGE_KEY);
|
||||
if (raw) return JSON.parse(raw) as Message[];
|
||||
} catch {}
|
||||
return [makeMsg("bot", "Hi! What can I help you with today?")];
|
||||
}
|
||||
|
||||
export function ChatbotButton() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [step, setStep] = useState<Step>("menu");
|
||||
const [messages, setMessages] = useState<Message[]>(INITIAL_MESSAGES);
|
||||
const [messages, setMessages] = useState<Message[]>(loadSavedMessages);
|
||||
const [pasteInput, setPasteInput] = useState("");
|
||||
const [parseError, setParseError] = useState("");
|
||||
const [eligibilityData, setEligibilityData] = useState<EligibilityData | null>(null);
|
||||
@@ -112,6 +120,12 @@ export function ChatbotButton() {
|
||||
const [eligibilityIdData, setEligibilityIdData] = useState<{ memberId: string; dob: string; siteKey: string; autoCheck: string; patient: PatientResult | null } | null>(null);
|
||||
const [checkAndClaimData, setCheckAndClaimData] = useState<CheckAndClaimData | null>(null);
|
||||
const [clarificationData, setClarificationData] = useState<{ memberId: string; dob: string; patient: PatientResult | null; procedureNames: string[]; options: string[] } | null>(null);
|
||||
const [apptSelectionData, setApptSelectionData] = useState<{
|
||||
patient: PatientResult;
|
||||
siteKey: string;
|
||||
matchedCodes: { code: string; description: string }[];
|
||||
options: { label: string; appointmentId: number; serviceDate: string }[];
|
||||
} | null>(null);
|
||||
const [, setLocation] = useLocation();
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const pasteRef = useRef<HTMLTextAreaElement>(null);
|
||||
@@ -121,6 +135,14 @@ export function ChatbotButton() {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}, [messages, step]);
|
||||
|
||||
// Persist messages across navigation (cleared on logout)
|
||||
useEffect(() => {
|
||||
try {
|
||||
const saveable = messages.filter((m) => !m.isLoading);
|
||||
sessionStorage.setItem(CHAT_STORAGE_KEY, JSON.stringify(saveable));
|
||||
} catch {}
|
||||
}, [messages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (step === "eligibility-input") {
|
||||
setTimeout(() => pasteRef.current?.focus(), 50);
|
||||
@@ -141,9 +163,9 @@ export function ChatbotButton() {
|
||||
return next;
|
||||
});
|
||||
|
||||
const reset = () => {
|
||||
// Resets step/data only — keeps message history
|
||||
const resetStep = () => {
|
||||
setStep("menu");
|
||||
setMessages([makeMsg("bot", "Hi! What can I help you with today?")]);
|
||||
setPasteInput("");
|
||||
setParseError("");
|
||||
setEligibilityData(null);
|
||||
@@ -152,22 +174,33 @@ export function ChatbotButton() {
|
||||
setEligibilityIdData(null);
|
||||
setCheckAndClaimData(null);
|
||||
setClarificationData(null);
|
||||
setApptSelectionData(null);
|
||||
};
|
||||
|
||||
// Full reset including message history and stored session
|
||||
const reset = () => {
|
||||
resetStep();
|
||||
const fresh = [makeMsg("bot", "Hi! What can I help you with today?")];
|
||||
try {
|
||||
sessionStorage.setItem(CHAT_STORAGE_KEY, JSON.stringify(fresh));
|
||||
} catch {}
|
||||
setMessages(fresh);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
reset();
|
||||
resetStep();
|
||||
};
|
||||
|
||||
const handleOptionSelect = (option: "eligibility" | "schedule" | "claims") => {
|
||||
if (option === "schedule") {
|
||||
addMsg("user", "Schedule an appointment");
|
||||
addMsg("bot", "Opening the appointments page...");
|
||||
setTimeout(() => { setLocation("/appointments"); setOpen(false); reset(); }, 600);
|
||||
setTimeout(() => { setLocation("/appointments"); setOpen(false); resetStep(); }, 600);
|
||||
} else if (option === "claims") {
|
||||
addMsg("user", "View claims");
|
||||
addMsg("bot", "Opening the claims page...");
|
||||
setTimeout(() => { setLocation("/claims"); setOpen(false); reset(); }, 600);
|
||||
setTimeout(() => { setLocation("/claims"); setOpen(false); resetStep(); }, 600);
|
||||
} else if (option === "eligibility") {
|
||||
addMsg("user", "Check Eligibility");
|
||||
addMsg("bot", "Please enter the patient's Member ID and Date of Birth:");
|
||||
@@ -199,7 +232,7 @@ export function ChatbotButton() {
|
||||
autoCheck: getAutoCheck(eligibilityData.dobISO),
|
||||
}));
|
||||
window.dispatchEvent(new CustomEvent("chatbot:eligibility-prefill"));
|
||||
setTimeout(() => { setLocation("/insurance-status"); setOpen(false); reset(); }, 600);
|
||||
setTimeout(() => { setLocation("/insurance-status"); setOpen(false); resetStep(); }, 600);
|
||||
};
|
||||
|
||||
const handleEligibilityFromPatient = () => {
|
||||
@@ -214,13 +247,13 @@ export function ChatbotButton() {
|
||||
}));
|
||||
window.dispatchEvent(new CustomEvent("chatbot:eligibility-prefill"));
|
||||
}
|
||||
setTimeout(() => { setLocation("/insurance-status"); setOpen(false); reset(); }, 600);
|
||||
setTimeout(() => { setLocation("/insurance-status"); setOpen(false); resetStep(); }, 600);
|
||||
};
|
||||
|
||||
const prefillAndNavigate = (memberId: string, dobISO: string, autoCheck: string) => {
|
||||
sessionStorage.setItem("chatbot_eligibility", JSON.stringify({ memberId, dob: dobISO, autoCheck }));
|
||||
window.dispatchEvent(new CustomEvent("chatbot:eligibility-prefill"));
|
||||
setTimeout(() => { setLocation("/insurance-status"); setOpen(false); reset(); }, 600);
|
||||
setTimeout(() => { setLocation("/insurance-status"); setOpen(false); resetStep(); }, 600);
|
||||
};
|
||||
|
||||
const handleEligibilityIdRun = () => {
|
||||
@@ -257,13 +290,17 @@ export function ChatbotButton() {
|
||||
setStep("ai-loading");
|
||||
|
||||
try {
|
||||
const res = await apiRequest("POST", "/api/ai/internal-chat", { message: text });
|
||||
const history = messages
|
||||
.filter((m) => !m.isLoading)
|
||||
.slice(-15)
|
||||
.map((m) => ({ role: m.role === "user" ? "user" : "assistant", text: m.text }));
|
||||
const res = await apiRequest("POST", "/api/ai/internal-chat", { message: text, history });
|
||||
const data = await res.json();
|
||||
|
||||
replaceLastMsg(data.reply ?? "Sorry, I couldn't process that.");
|
||||
|
||||
if (data.action === "navigate" && data.actionData?.url) {
|
||||
setTimeout(() => { setLocation(data.actionData.url); setOpen(false); reset(); }, 800);
|
||||
setTimeout(() => { setLocation(data.actionData.url); setOpen(false); resetStep(); }, 800);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -313,6 +350,41 @@ export function ChatbotButton() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.action === "appointment_created") {
|
||||
setStep("menu");
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.action === "need_appointment_selection" && data.actionData) {
|
||||
setApptSelectionData({
|
||||
patient: data.actionData.patient,
|
||||
siteKey: data.actionData.siteKey,
|
||||
matchedCodes: data.actionData.matchedCodes ?? [],
|
||||
options: data.actionData.options ?? [],
|
||||
});
|
||||
setStep("need-appointment-selection");
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.action === "claim_only_ready" && data.actionData) {
|
||||
const { patient, matchedCodes, siteKey, serviceDate, appointmentId } = data.actionData;
|
||||
if (patient?.id && matchedCodes?.length > 0) {
|
||||
sessionStorage.setItem(
|
||||
"chatbot_claim_prefill",
|
||||
JSON.stringify({ codes: matchedCodes, siteKey, serviceDate, autoSubmit: true })
|
||||
);
|
||||
}
|
||||
const url = appointmentId
|
||||
? `/claims?appointmentId=${appointmentId}`
|
||||
: `/claims?newPatient=${patient?.id}`;
|
||||
setTimeout(() => {
|
||||
setLocation(url);
|
||||
setOpen(false);
|
||||
resetStep();
|
||||
}, 600);
|
||||
return;
|
||||
}
|
||||
|
||||
setStep("menu");
|
||||
} catch {
|
||||
replaceLastMsg("Sorry, something went wrong. Please try again.");
|
||||
@@ -333,7 +405,8 @@ export function ChatbotButton() {
|
||||
step === "patient-found" ||
|
||||
step === "eligibility-id-ready" ||
|
||||
step === "check-and-claim-ready" ||
|
||||
step === "need-insurance-clarification";
|
||||
step === "need-insurance-clarification" ||
|
||||
step === "need-appointment-selection";
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -360,13 +433,24 @@ export function ChatbotButton() {
|
||||
<Bot className="h-4 w-4" />
|
||||
<span className="font-semibold text-sm">Assistant</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="hover:opacity-70 transition-opacity rounded"
|
||||
aria-label="Close"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={reset}
|
||||
className="flex items-center gap-1 text-xs hover:opacity-70 transition-opacity rounded px-1.5 py-0.5 border border-white/30 hover:bg-white/10"
|
||||
title="Clear chat history"
|
||||
>
|
||||
<RotateCcw className="h-3 w-3" />
|
||||
Clear
|
||||
</button>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="hover:opacity-70 transition-opacity rounded"
|
||||
aria-label="Close"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
@@ -631,6 +715,44 @@ export function ChatbotButton() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Appointment selection */}
|
||||
{step === "need-appointment-selection" && apptSelectionData && (
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-3 space-y-2">
|
||||
<p className="text-xs font-semibold text-amber-800">Which appointment date?</p>
|
||||
<div className="flex flex-col gap-1.5 pt-1">
|
||||
{apptSelectionData.options.map((opt) => (
|
||||
<button
|
||||
key={opt.serviceDate}
|
||||
className="text-left text-xs px-3 py-2 rounded-lg border border-amber-300 hover:bg-amber-100 transition-colors font-medium"
|
||||
onClick={() => {
|
||||
addMsg("user", opt.label);
|
||||
addMsg("bot", `Using ${opt.label} as the service date.`);
|
||||
sessionStorage.setItem(
|
||||
"chatbot_claim_prefill",
|
||||
JSON.stringify({
|
||||
codes: apptSelectionData.matchedCodes,
|
||||
siteKey: apptSelectionData.siteKey,
|
||||
serviceDate: opt.serviceDate,
|
||||
autoSubmit: true,
|
||||
})
|
||||
);
|
||||
setTimeout(() => {
|
||||
setLocation(`/claims?appointmentId=${opt.appointmentId}`);
|
||||
setOpen(false);
|
||||
resetStep();
|
||||
}, 600);
|
||||
}}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<Button size="sm" variant="ghost" className="h-7 text-xs w-full" onClick={resetStep}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user