From 46daeb1c1fcc75f5fd8030a98c3bf67b62f3918b Mon Sep 17 00:00:00 2001 From: ff Date: Wed, 10 Jun 2026 22:00:46 -0400 Subject: [PATCH] feat: implement Claim for Column with AI queue logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sequential AI-assisted claim flow: - Filters unclaimed appointments in selected columns - Calls /api/ai/internal-chat to interpret appointment notes into CDT codes - Shows per-appointment confirmation modal (patient name, notes, matched codes) - Confirm → stores chatbot_claim_prefill + navigates to claims page for Selenium - Skip → moves to next appointment - Queue persisted in sessionStorage; auto-resumes on return to schedule page Co-Authored-By: Claude Sonnet 4.6 --- apps/Frontend/src/pages/appointments-page.tsx | 246 +++++++++++++++++- 1 file changed, 245 insertions(+), 1 deletion(-) diff --git a/apps/Frontend/src/pages/appointments-page.tsx b/apps/Frontend/src/pages/appointments-page.tsx index aaccc041..418210ee 100755 --- a/apps/Frontend/src/pages/appointments-page.tsx +++ b/apps/Frontend/src/pages/appointments-page.tsx @@ -167,6 +167,20 @@ export default function AppointmentsPage() { const [isClaimingColumn, setIsClaimingColumn] = useState(false); const [selectedClaimAiColumns, setSelectedClaimAiColumns] = useState>(new Set()); const [isClaimingAiColumn, setIsClaimingAiColumn] = useState(false); + const [aiClaimModalOpen, setAiClaimModalOpen] = useState(false); + const [aiClaimQueue, setAiClaimQueue] = useState([]); + const [aiClaimCurrentIndex, setAiClaimCurrentIndex] = useState(0); + const [isAiClaimProcessing, setIsAiClaimProcessing] = useState(false); + const [aiClaimCurrentData, setAiClaimCurrentData] = useState<{ + matchedCodes: Array<{ code: string; description: string; toothNumber?: string; toothSurface?: string }>; + siteKey: string; + serviceDate: string; + appointmentId: number; + patientName: string; + notes: string; + reply: string; + } | null>(null); + const [needsAiClaimResume, setNeedsAiClaimResume] = useState(false); const [selectedReminderColumns, setSelectedReminderColumns] = useState>(new Set()); const [isSendingReminders, setIsSendingReminders] = useState(false); const [reminderAiFollowUp, setReminderAiFollowUp] = useState(true); @@ -404,6 +418,40 @@ export default function AppointmentsPage() { } }, [location]); + // On mount: detect if we should resume an AI claim queue after returning from claims page + useEffect(() => { + try { + const raw = sessionStorage.getItem("ai_claim_queue"); + if (!raw) return; + const parsed = JSON.parse(raw); + if (parsed?.pendingResume && Array.isArray(parsed.appointments) && parsed.appointments.length > 0) { + setNeedsAiClaimResume(true); + } + } catch { + sessionStorage.removeItem("ai_claim_queue"); + } + }, []); + + // When appointments finish loading and a resume is pending, reopen the modal + useEffect(() => { + if (!needsAiClaimResume || isLoadingAppointments) return; + setNeedsAiClaimResume(false); + try { + const raw = sessionStorage.getItem("ai_claim_queue"); + if (!raw) return; + const parsed = JSON.parse(raw); + const queue = parsed.appointments as ScheduledAppointment[]; + if (!queue?.length) { sessionStorage.removeItem("ai_claim_queue"); return; } + sessionStorage.setItem("ai_claim_queue", JSON.stringify({ ...parsed, pendingResume: false })); + setAiClaimQueue(queue); + setAiClaimCurrentIndex(0); + setAiClaimModalOpen(true); + processAiClaimAtIndex(queue, 0); + } catch { + sessionStorage.removeItem("ai_claim_queue"); + } + }, [needsAiClaimResume, isLoadingAppointments]); + // Create/upsert appointment mutation const createAppointmentMutation = useMutation({ mutationFn: async (appointment: InsertAppointment) => { @@ -1323,6 +1371,124 @@ export default function AppointmentsPage() { } }; + // ── AI Claim Queue ───────────────────────────────────────────────────────── + + const processAiClaimAtIndex = async (queue: ScheduledAppointment[], index: number) => { + if (index >= queue.length) { + setAiClaimModalOpen(false); + sessionStorage.removeItem("ai_claim_queue"); + toast({ title: "All Done", description: `Finished processing all ${queue.length} appointment${queue.length !== 1 ? "s" : ""}.` }); + return; + } + const apt = queue[index]!; + const aptDate = typeof apt.date === "string" ? apt.date : formatLocalDate(apt.date as Date); + if (!apt.notes?.trim()) { + setIsAiClaimProcessing(false); + setAiClaimCurrentData({ + matchedCodes: [], + siteKey: "", + serviceDate: aptDate, + appointmentId: Number(apt.id), + patientName: apt.patientName, + notes: "", + reply: "No notes on this appointment.", + }); + return; + } + setIsAiClaimProcessing(true); + setAiClaimCurrentData(null); + try { + const res = await apiRequest("POST", "/api/ai/internal-chat", { + message: `claim ${apt.notes} for ${apt.patientName}`, + clientDate: aptDate, + }); + const data = await res.json(); + if ((data.action === "claim_only_ready" || data.action === "check_and_claim_ready") && data.actionData) { + setAiClaimCurrentData({ + matchedCodes: data.actionData.matchedCodes ?? [], + siteKey: data.actionData.siteKey ?? "", + serviceDate: data.actionData.serviceDate ?? aptDate, + appointmentId: Number(apt.id), + patientName: apt.patientName, + notes: apt.notes ?? "", + reply: data.reply ?? "", + }); + } else { + setAiClaimCurrentData({ + matchedCodes: [], + siteKey: "", + serviceDate: aptDate, + appointmentId: Number(apt.id), + patientName: apt.patientName, + notes: apt.notes ?? "", + reply: data.reply ?? "Could not interpret notes.", + }); + } + } catch { + setAiClaimCurrentData({ + matchedCodes: [], + siteKey: "", + serviceDate: aptDate, + appointmentId: Number(apt.id), + patientName: apt.patientName, + notes: apt.notes ?? "", + reply: "Error contacting AI.", + }); + } finally { + setIsAiClaimProcessing(false); + } + }; + + const handleClaimForColumnWithAi = async () => { + if (selectedClaimAiColumns.size === 0) return; + const unclaimed = processedAppointments.filter( + (apt) => selectedClaimAiColumns.has(apt.staffId) && !apt.hasClaimWithNumber + ); + if (!unclaimed.length) { + toast({ title: "No unclaimed appointments", description: "All appointments in the selected columns are already claimed." }); + return; + } + sessionStorage.setItem("ai_claim_queue", JSON.stringify({ + appointments: unclaimed, + date: formattedSelectedDate, + pendingResume: false, + })); + setAiClaimQueue(unclaimed); + setAiClaimCurrentIndex(0); + setAiClaimModalOpen(true); + await processAiClaimAtIndex(unclaimed, 0); + }; + + const handleAiClaimConfirm = () => { + if (!aiClaimCurrentData) return; + const { matchedCodes, siteKey, serviceDate, appointmentId } = aiClaimCurrentData; + const nextIndex = aiClaimCurrentIndex + 1; + const remaining = aiClaimQueue.slice(nextIndex); + if (remaining.length > 0) { + sessionStorage.setItem("ai_claim_queue", JSON.stringify({ + appointments: remaining, + date: formattedSelectedDate, + pendingResume: true, + })); + } else { + sessionStorage.removeItem("ai_claim_queue"); + } + sessionStorage.setItem("chatbot_claim_prefill", JSON.stringify({ + codes: matchedCodes, + siteKey, + serviceDate, + autoSubmit: true, + })); + setAiClaimModalOpen(false); + setLocation(`/claims?appointmentId=${appointmentId}`); + }; + + const handleAiClaimSkip = async () => { + const nextIndex = aiClaimCurrentIndex + 1; + setAiClaimCurrentIndex(nextIndex); + await processAiClaimAtIndex(aiClaimQueue, nextIndex); + }; + return (
+ )} + + +
+ + ) : null} + + + + )} ); }