feat: AI SMS reminder flow with two-message intro, smart reschedule with availability checks

- Reminder flow: send AI self-introduction as message 1 (Twilio REST API), intent response as message 2 (TwiML) so intro always arrives first
- LangGraph reminder graph: classify yes/no/other from patient reply; 'no' now asks 'When would you like to reschedule?' directly
- Reschedule flow: new asked_reschedule_datetime stage replaces multi-step ASAP/next-week flow
  - Date-only reply (e.g. '5/18'): ask for time separately, then confirm
  - Date+time reply (e.g. '5/18 at 10am'): go straight to confirmation
  - new asked_reschedule_time_for_date and asked_reschedule_confirm_datetime stages
- Date/time parsing: regex handles M/D and am/pm formats first; falls back to Gemini for natural language
- Day-level office hours check: if requested day is closed (e.g. Sunday), reply 'Our office is closed on [date]. Choose another day?'
- Time-level office hours check: if requested time is outside working hours (e.g. 12pm during lunch), reply with actual hours (e.g. '9:00 am – 12:00 pm and 1:00 pm – 5:00 pm')
- Slot availability check: verifies no conflicting appointment for same staff member
- After appointment confirmed: patient thank-you reply triggers warm closing with upcoming appointment time
- Schedule page: office hours summary bar above grid showing today's configured hours with link to settings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gitead
2026-05-11 16:01:23 -04:00
parent 585b448b6e
commit 1ff843bc79
5 changed files with 842 additions and 149 deletions

View File

@@ -27,6 +27,8 @@ import {
Stethoscope,
Download,
MessageSquare,
Clock,
ExternalLink,
} from "lucide-react";
import { useToast } from "@/hooks/use-toast";
import { Calendar } from "@/components/ui/calendar";
@@ -1681,6 +1683,53 @@ export default function AppointmentsPage() {
</div>
</div>
{/* Office Hours Summary */}
{(() => {
const dayNames = ["sunday","monday","tuesday","wednesday","thursday","friday","saturday"] as const;
const dayName = dayNames[selectedDate.getDay()]!;
const fmt = (t: string) => {
const [hh, mm] = t.split(":").map(Number);
const period = (hh ?? 0) >= 12 ? "PM" : "AM";
const h12 = (hh ?? 0) > 12 ? (hh ?? 0) - 12 : (hh ?? 0) === 0 ? 12 : (hh ?? 0);
return `${h12}:${String(mm ?? 0).padStart(2,"0")} ${period}`;
};
const doctorHours = officeHours?.doctors?.[dayName];
const hygHours = officeHours?.hygienists?.[dayName];
const isOverride = officeHours?.overrideDates?.includes(selectedDate.toLocaleDateString("en-CA"));
return (
<div className="px-4 pb-3 flex items-center gap-4 flex-wrap text-xs text-gray-500 border-b">
<div className="flex items-center gap-1.5 font-medium text-gray-700">
<Clock className="h-3.5 w-3.5 text-teal-600" />
<span>Office Hours</span>
<button onClick={() => setLocation("/settings/officehours")} className="ml-1 text-teal-600 hover:text-teal-700" title="Edit office hours">
<ExternalLink className="h-3 w-3" />
</button>
</div>
{!officeHours ? (
<span className="italic text-gray-400">Not configured <button onClick={() => setLocation("/settings/officehours")} className="text-teal-600 underline">set up office hours</button></span>
) : isOverride ? (
<span className="text-teal-600 font-medium">Override active all slots open today</span>
) : (
<>
<span>
<span className="font-medium text-gray-600">Doctors (AC):</span>{" "}
{doctorHours?.enabled
? `${fmt(doctorHours.amStart)}${fmt(doctorHours.amEnd)}, ${fmt(doctorHours.pmStart)}${fmt(doctorHours.pmEnd)}`
: <span className="text-gray-400">Closed</span>}
</span>
<span>
<span className="font-medium text-gray-600">Hygienists (DF):</span>{" "}
{hygHours?.enabled
? `${fmt(hygHours.amStart)}${fmt(hygHours.amEnd)}, ${fmt(hygHours.pmStart)}${fmt(hygHours.pmEnd)}`
: <span className="text-gray-400">Closed</span>}
</span>
</>
)}
</div>
);
})()}
{/* Schedule Grid with Drag and Drop */}
<DndProvider backend={HTML5Backend}>
<div className="overflow-x-auto">