feat: schedule page SMS reminders with AI follow-up and reschedule column

- Add Text Reminder for Column button with per-column checkboxes and AI follow-up toggle (default on)
- Batch reminder endpoint resolves {firstName}, {officeName}, {appointmentDate}, {appointmentTime} from AI chat templates
- Add Reschedule for Column UI (logic TBD)
- Move Download Claim PDF for Column below Reschedule for Column
- Add reminderSms template field to AI Chat Settings with variable hints

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gitead
2026-05-09 23:17:05 -04:00
parent 112529155c
commit 585b448b6e
5 changed files with 226 additions and 7 deletions

View File

@@ -158,6 +158,9 @@ export default function AppointmentsPage() {
const [selectedClaimColumns, setSelectedClaimColumns] = useState<Set<number>>(new Set());
const [isClaimingColumn, setIsClaimingColumn] = useState(false);
const [selectedReminderColumns, setSelectedReminderColumns] = useState<Set<number>>(new Set());
const [isSendingReminders, setIsSendingReminders] = useState(false);
const [reminderAiFollowUp, setReminderAiFollowUp] = useState(true);
const [selectedRescheduleColumns, setSelectedRescheduleColumns] = useState<Set<number>>(new Set());
const [selectedDownloadPdfColumns, setSelectedDownloadPdfColumns] = useState<Set<number>>(new Set());
const [isDownloadingClaimPdfs, setIsDownloadingClaimPdfs] = useState(false);
const [columnLabels, setColumnLabels] = useState<Record<string, string>>({});
@@ -187,6 +190,15 @@ export default function AppointmentsPage() {
});
};
const toggleRescheduleColumn = (staffId: number) => {
setSelectedRescheduleColumns((prev) => {
const next = new Set(prev);
if (next.has(staffId)) next.delete(staffId);
else next.add(staffId);
return next;
});
};
const toggleDownloadPdfColumn = (staffId: number) => {
setSelectedDownloadPdfColumns((prev) => {
const next = new Set(prev);
@@ -1202,6 +1214,27 @@ export default function AppointmentsPage() {
}
};
const handleSendRemindersForColumn = async () => {
if (!user || selectedReminderColumns.size === 0) return;
setIsSendingReminders(true);
try {
const res = await apiRequest("POST", "/api/twilio/send-reminders-batch", {
date: formattedSelectedDate,
staffIds: Array.from(selectedReminderColumns),
aiFollowUp: reminderAiFollowUp,
});
const { sent, skipped } = await res.json();
toast({
title: "Text Reminders Sent",
description: `Sent ${sent} reminder${sent !== 1 ? "s" : ""}${skipped > 0 ? `, skipped ${skipped} (no phone)` : ""}.`,
});
} catch (err: any) {
toast({ title: "Failed to Send Reminders", description: err?.message ?? String(err), variant: "destructive" });
} finally {
setIsSendingReminders(false);
}
};
const handleDownloadClaimPdfs = async () => {
if (!user || selectedDownloadPdfColumns.size === 0) return;
const staffIdsParam = Array.from(selectedDownloadPdfColumns).join(",");
@@ -1349,10 +1382,21 @@ export default function AppointmentsPage() {
{/* Text Reminder 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={() => handleSendRemindersForColumn()}
disabled={isLoading || isSendingReminders || selectedReminderColumns.size === 0}
size="sm"
>
Text Reminder for Column
{isSendingReminders ? (
<>
<LoaderCircleIcon className="h-4 w-4 mr-1 animate-spin" />
Sending...
</>
) : (
<>
<MessageSquare className="h-4 w-4 mr-1" />
Text Reminder for Column
</>
)}
</Button>
{staffMembers.map((staff, index) => (
<label
@@ -1370,6 +1414,46 @@ 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={reminderAiFollowUp}
onClick={() => setReminderAiFollowUp((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 ${reminderAiFollowUp ? "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 ${reminderAiFollowUp ? "translate-x-4" : "translate-x-0"}`}
/>
</button>
<span className="text-xs text-gray-600 whitespace-nowrap">AI follow up</span>
</div>
</div>
{/* 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}
size="sm"
>
Reschedule for Column
</Button>
{staffMembers.map((staff, index) => (
<label
key={staff.id}
className="flex items-center gap-1 cursor-pointer select-none"
>
<input
type="checkbox"
className="w-4 h-4 rounded border-gray-400 accent-teal-600"
checked={selectedRescheduleColumns.has(Number(staff.id))}
onChange={() => toggleRescheduleColumn(Number(staff.id))}
/>
<span className="text-sm font-medium">
{columnLabels[String(staff.id)] ?? String.fromCharCode(65 + index)}
</span>
</label>
))}
</div>
{/* Download Claim PDF for Column section */}