feat: reschedule-by-office batch SMS, AI follow-up toggle, date-shortcut fix, combined flow diagram
- Add Reschedule for Column button on schedule page with AI follow-up toggle (default on)
- Add POST /api/twilio/send-reschedule-batch — sends Reschedule by Office template, starts AI reschedule conversation per patient
- Add {officePhone} (office call-in number) and {twilioPhone} (SMS number) variable replacement in both batch endpoints
- Fix broken variable names in Reschedule by Office template ({office phone number) → {officePhone}, {Twilio phone number} → {twilioPhone})
- Fix reschedule-graph: when patient replies with date in same message as YES/NO (e.g. "ok, 5/18"), AI now checks day open and asks for time instead of asking "what day and time?"
- Fix twilio-webhooks: same date-shortcut logic for reminder flow — "no, 5/18" skips "when to reschedule?" and goes straight to day check
- Update LangGraph SVG: rename to Reminder & Reschedule Flow, combine both entry points (Reminder SMS + Reschedule SMS) into one diagram with date-shortcut annotations
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -166,6 +166,8 @@ export default function AppointmentsPage() {
|
||||
const [isSendingReminders, setIsSendingReminders] = useState(false);
|
||||
const [reminderAiFollowUp, setReminderAiFollowUp] = useState(true);
|
||||
const [selectedRescheduleColumns, setSelectedRescheduleColumns] = useState<Set<number>>(new Set());
|
||||
const [isSendingReschedule, setIsSendingReschedule] = useState(false);
|
||||
const [rescheduleAiFollowUp, setRescheduleAiFollowUp] = useState(true);
|
||||
const [selectedDownloadPdfColumns, setSelectedDownloadPdfColumns] = useState<Set<number>>(new Set());
|
||||
const [isDownloadingClaimPdfs, setIsDownloadingClaimPdfs] = useState(false);
|
||||
const [columnLabels, setColumnLabels] = useState<Record<string, string>>({});
|
||||
@@ -1260,6 +1262,27 @@ export default function AppointmentsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendRescheduleForColumn = async () => {
|
||||
if (!user || selectedRescheduleColumns.size === 0) return;
|
||||
setIsSendingReschedule(true);
|
||||
try {
|
||||
const res = await apiRequest("POST", "/api/twilio/send-reschedule-batch", {
|
||||
date: formattedSelectedDate,
|
||||
staffIds: Array.from(selectedRescheduleColumns),
|
||||
aiFollowUp: rescheduleAiFollowUp,
|
||||
});
|
||||
const { sent, skipped } = await res.json();
|
||||
toast({
|
||||
title: "Reschedule Messages Sent",
|
||||
description: `Sent ${sent} message${sent !== 1 ? "s" : ""}${skipped > 0 ? `, skipped ${skipped} (no phone)` : ""}.`,
|
||||
});
|
||||
} catch (err: any) {
|
||||
toast({ title: "Failed to Send Reschedule Messages", description: err?.message ?? String(err), variant: "destructive" });
|
||||
} finally {
|
||||
setIsSendingReschedule(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadClaimPdfs = async () => {
|
||||
if (!user || selectedDownloadPdfColumns.size === 0) return;
|
||||
const staffIdsParam = Array.from(selectedDownloadPdfColumns).join(",");
|
||||
@@ -1458,10 +1481,21 @@ export default function AppointmentsPage() {
|
||||
{/* Reschedule for Column section */}
|
||||
<div className="flex items-center gap-2 border rounded-md px-3 py-2 bg-white shadow-sm">
|
||||
<Button
|
||||
disabled={true}
|
||||
onClick={() => handleSendRescheduleForColumn()}
|
||||
disabled={isLoading || isSendingReschedule || selectedRescheduleColumns.size === 0}
|
||||
size="sm"
|
||||
>
|
||||
Reschedule for Column
|
||||
{isSendingReschedule ? (
|
||||
<>
|
||||
<LoaderCircleIcon className="h-4 w-4 mr-1 animate-spin" />
|
||||
Sending...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<MessageSquare className="h-4 w-4 mr-1" />
|
||||
Reschedule for Column
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
{staffMembers.map((staff, index) => (
|
||||
<label
|
||||
@@ -1479,6 +1513,20 @@ export default function AppointmentsPage() {
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
<div className="flex items-center gap-1.5 ml-2 pl-2 border-l border-gray-200">
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={rescheduleAiFollowUp}
|
||||
onClick={() => setRescheduleAiFollowUp((v) => !v)}
|
||||
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors focus:outline-none ${rescheduleAiFollowUp ? "bg-teal-600" : "bg-gray-300"}`}
|
||||
>
|
||||
<span
|
||||
className={`pointer-events-none inline-block h-4 w-4 rounded-full bg-white shadow transform transition-transform ${rescheduleAiFollowUp ? "translate-x-4" : "translate-x-0"}`}
|
||||
/>
|
||||
</button>
|
||||
<span className="text-xs text-gray-600 whitespace-nowrap">AI follow up</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Download Claim PDF for Column section */}
|
||||
|
||||
Reference in New Issue
Block a user