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:
Gitead
2026-05-13 00:36:38 -04:00
parent 7929dc6e19
commit 131733564e
5 changed files with 371 additions and 78 deletions

View File

@@ -837,8 +837,84 @@ export async function runRescheduleStep(
return { reply, nextStage: "done" };
}
// Patient confirmed they want to reschedule — move straight to datetime request
// Patient confirmed they want to reschedule.
// First check if they already included a date (and optionally time) in the same message.
if (yes(t)) {
const hasTime = messageHasTime(message);
if (hasTime) {
// Try to parse full datetime from the message
const parsed = await parseDatetimeFromMessage(message, apiKey);
if (parsed) {
const { date, startTime, displayLabel } = parsed;
const dayCheck = await isOfficeDayOpen(date, userId);
if (!dayCheck.open) {
const datePart = displayLabel.split(" at ")[0] ?? displayLabel;
const fallbacks: Record<string, string> = {
English: `Our office is closed on ${datePart} (${dayCheck.displayDay}). Can you please choose another day?`,
Spanish: `Nuestra oficina está cerrada el ${datePart} (${dayCheck.displayDay}). ¿Puede elegir otro día?`,
Portuguese: `Nosso consultório está fechado em ${datePart} (${dayCheck.displayDay}). Pode escolher outro dia?`,
Mandarin: `我们诊所在 ${datePart}${dayCheck.displayDay})不开放。请您选择另一天好吗?`,
Cantonese: `我們診所在 ${datePart}${dayCheck.displayDay})不開放。請您選擇另一天好嗎?`,
Arabic: `مكتبنا مغلق في ${datePart} (${dayCheck.displayDay}). هل يمكنك اختيار يوم آخر؟`,
"Haitian Creole": `Biwo nou fèmen nan ${datePart} (${dayCheck.displayDay}). Tanpri chwazi yon lòt jou?`,
};
return { reply: fallbacks[lang] ?? fallbacks["English"]!, nextStage: "asked_reschedule_datetime" };
}
setPendingReschedule(userId, patientId, { newDate: date, dayLabel: displayLabel, startTime });
const fallbacks: Record<string, string> = {
English: `Just to confirm — do you prefer ${displayLabel}?`,
Spanish: `Solo para confirmar — ¿prefiere el ${displayLabel}?`,
Portuguese: `Só para confirmar — você prefere ${displayLabel}?`,
Mandarin: `确认一下——您希望的时间是 ${displayLabel} 吗?`,
Cantonese: `確認一下——您希望的時間是 ${displayLabel} 嗎?`,
Arabic: `فقط للتأكيد — هل تفضل ${displayLabel}؟`,
"Haitian Creole": `Jis pou konfime — èske ou prefere ${displayLabel}?`,
};
const confirmReply = await llmReply(
`You are a friendly dental office AI assistant. The patient mentioned a date/time that you interpreted as "${displayLabel}". Ask them in ${lang} to confirm. 1 sentence, natural and friendly. No formatting.`,
`Patient said: "${message}"`, fallbacks[lang] ?? fallbacks["English"]!, apiKey,
);
return { reply: confirmReply, nextStage: "asked_reschedule_confirm_datetime" };
}
}
// Try to parse a date-only from the message
const parsedDate = await parseDateOnlyFromMessage(message, apiKey);
if (parsedDate) {
const { date, dateLabel } = parsedDate;
const dayCheck = await isOfficeDayOpen(date, userId);
if (!dayCheck.open) {
const fallbacks: Record<string, string> = {
English: `Our office is closed on ${dateLabel} (${dayCheck.displayDay}). Can you please choose another day?`,
Spanish: `Nuestra oficina está cerrada el ${dateLabel} (${dayCheck.displayDay}). ¿Puede elegir otro día?`,
Portuguese: `Nosso consultório está fechado em ${dateLabel} (${dayCheck.displayDay}). Pode escolher outro dia?`,
Mandarin: `我们诊所在 ${dateLabel}${dayCheck.displayDay})不开放。请您选择另一天好吗?`,
Cantonese: `我們診所在 ${dateLabel}${dayCheck.displayDay})不開放。請您選擇另一天好嗎?`,
Arabic: `مكتبنا مغلق في ${dateLabel} (${dayCheck.displayDay}). هل يمكنك اختيار يوم آخر؟`,
"Haitian Creole": `Biwo nou fèmen nan ${dateLabel} (${dayCheck.displayDay}). Tanpri chwazi yon lòt jou?`,
};
return { reply: fallbacks[lang] ?? fallbacks["English"]!, nextStage: "asked_reschedule_datetime" };
}
// Day is open — save and ask for time
setPendingReschedule(userId, patientId, { newDate: date, dayLabel: dateLabel });
const fallbacks: Record<string, string> = {
English: `What time do you prefer on ${dateLabel}?`,
Spanish: `¿A qué hora prefiere el ${dateLabel}?`,
Portuguese: `Que horário você prefere em ${dateLabel}?`,
Mandarin: `您希望在 ${dateLabel} 几点?`,
Cantonese: `您希望在 ${dateLabel} 幾點?`,
Arabic: `ما الوقت الذي تفضله في ${dateLabel}؟`,
"Haitian Creole": `Ki lè ou prefere nan ${dateLabel}?`,
};
const askTimeReply = await llmReply(
`You are a friendly dental office assistant. The patient wants ${dateLabel}. Ask them in ${lang} what time they prefer on that day. 1 sentence, no formatting.`,
`Patient wants ${dateLabel} but gave no time.`, fallbacks[lang] ?? fallbacks["English"]!, apiKey,
);
return { reply: askTimeReply, nextStage: "asked_reschedule_time_for_date" };
}
// No date found — ask for day and time
const fallbacks: Record<string, string> = {
English: "What day and time would you like? For example: 'Monday at 10am' or 'next Tuesday afternoon'.",
Spanish: "¿Qué día y hora prefiere? Por ejemplo: 'lunes a las 10am' o 'martes por la tarde'.",