Files
DentalManagementMH07/apps/Backend/src/ai/aiHandoffStore.ts
T
ffandClaude Sonnet 5 cef928be97 feat: add AI recall calling — books existing patients for routine cleanings
Adds a new recall conversation flow (recall-graph.ts) triggered from the
Patient Connection "AI Call" button: offers an existing patient a routine
exam & cleaning, classifies their yes/no reply, then books directly onto
the schedule (Column A, 30-minute slots, real conflict checking against
office hours and existing appointments) instead of only capturing a
preference for staff to follow up on.

Also adds a "Voice call" label in the message thread to distinguish call
transcripts from SMS, and updates the Call Templates settings UI with an
editable recall greeting.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-02 23:13:09 -04:00

118 lines
4.3 KiB
TypeScript

import { prisma as db } from "@repo/db/client";
export type ConversationStage =
| "initial"
| "reminder_initial"
| "recall_initial"
| "recall_asked_datetime"
| "recall_asked_time_for_date"
| "greeted"
| "done"
| "new_patient_greeted"
| "asked_new_or_existing"
| "asked_new_patient_insurance"
| "asked_new_or_reschedule"
| "asked_insurance_type"
| "asked_masshealth_check_consent"
| "asked_existing_insurance"
| "asked_appointment_time"
| "awaiting_masshealth_info"
| "asked_appointment_preference"
| "asked_self_pay"
| "asked_other_insurance_after_inactive"
| "collecting_contact_info"
| "asked_new_appt_time_for_date"
| "asked_reschedule_confirm"
| "asked_reschedule_preference"
| "asked_reschedule_asap"
| "asked_reschedule_next_week"
| "asked_reschedule_time"
| "asked_reschedule_datetime"
| "asked_reschedule_time_for_date"
| "asked_reschedule_confirm_datetime";
// ── Conversation stage + AI handoff per patient (DB-persisted) ────────────────
export async function getStage(userId: number, patientId: number): Promise<ConversationStage> {
const row = await db.patientConversation.findUnique({ where: { patientId } });
return (row?.stage as ConversationStage) ?? "initial";
}
export async function setStage(userId: number, patientId: number, stage: ConversationStage): Promise<void> {
await db.patientConversation.upsert({
where: { patientId },
update: { stage },
create: { patientId, userId, stage, aiHandoff: true },
});
}
export async function getHandoff(userId: number, patientId: number): Promise<boolean> {
const row = await db.patientConversation.findUnique({ where: { patientId } });
return row?.aiHandoff ?? true;
}
export async function setHandoff(userId: number, patientId: number, enabled: boolean): Promise<void> {
await db.patientConversation.upsert({
where: { patientId },
update: { aiHandoff: enabled },
create: { patientId, userId, aiHandoff: enabled, stage: "initial" },
});
}
// ── After-hours handoff per user (persisted in ai_settings) ──────────────────
export async function getAfterHoursHandoff(userId: number): Promise<boolean> {
const row = await db.aiSettings.findUnique({ where: { userId } });
return row?.afterHoursEnabled ?? true;
}
export async function setAfterHoursHandoff(userId: number, enabled: boolean): Promise<void> {
await db.aiSettings.update({ where: { userId }, data: { afterHoursEnabled: enabled } });
}
// ── Conversation starters ─────────────────────────────────────────────────────
export async function resetConversation(userId: number, patientId: number): Promise<void> {
await setStage(userId, patientId, "reminder_initial");
}
export async function startNewPatientConversation(userId: number, patientId: number): Promise<void> {
await setStage(userId, patientId, "new_patient_greeted");
}
export async function startRescheduleConversation(userId: number, patientId: number): Promise<void> {
await setStage(userId, patientId, "asked_reschedule_confirm");
}
// Recall: office proactively calls/texts an existing patient to offer a
// routine exam & cleaning (typically covered at no cost by insurance).
export async function startRecallConversation(userId: number, patientId: number): Promise<void> {
await setStage(userId, patientId, "recall_initial");
}
// ── Pending reschedule (in-memory — seconds-lived within a single exchange) ───
interface PendingReschedule {
newDate: Date;
dayLabel: string;
startTime?: string; // "HH:MM" — set when full datetime was parsed together
}
const pendingRescheduleStore = new Map<string, PendingReschedule>();
function convKey(userId: number, patientId: number): string {
return `${userId}:${patientId}`;
}
export function setPendingReschedule(userId: number, patientId: number, data: PendingReschedule): void {
pendingRescheduleStore.set(convKey(userId, patientId), data);
}
export function getPendingReschedule(userId: number, patientId: number): PendingReschedule | undefined {
return pendingRescheduleStore.get(convKey(userId, patientId));
}
export function clearPendingReschedule(userId: number, patientId: number): void {
pendingRescheduleStore.delete(convKey(userId, patientId));
}