diff --git a/apps/Backend/src/routes/twilio-webhooks.ts b/apps/Backend/src/routes/twilio-webhooks.ts index 277ce5fd..8932669d 100644 --- a/apps/Backend/src/routes/twilio-webhooks.ts +++ b/apps/Backend/src/routes/twilio-webhooks.ts @@ -1,6 +1,7 @@ import express, { Request, Response } from "express"; import twilio from "twilio"; import { storage } from "../storage"; +import { DEFAULT_GREETING, DEFAULT_RECALL_GREETING } from "../storage/twilio-storage"; import { prisma as db } from "@repo/db/client"; import { runNewPatientStep } from "../ai/new-patient-graph"; import { @@ -663,6 +664,25 @@ function voiceLocaleFor(language: string): VoiceLocale { return { gatherLang: "en-US", sayVoice: "Polly.Joanna" }; } +// Spoken greeting is plain text, not LLM-generated, so it can't follow the +// patient's language on its own — translate it ourselves when the office +// hasn't overridden the default wording. Matches voiceLocaleFor's English/ +// Spanish-only voice coverage above. +const GREETING_TRANSLATIONS: Record = { + Spanish: "Hola {firstName}, soy Lisa, la asistente de inteligencia artificial de {officeName}. ¿En qué puedo ayudarle hoy?", +}; +const RECALL_GREETING_TRANSLATIONS: Record = { + Spanish: "Hola {firstName}, soy Lisa de {officeName}. Queríamos avisarle que ya le corresponde un examen y limpieza de rutina, que la mayoría de los seguros cubren sin costo. ¿Le gustaría programar una cita?", +}; + +/** Picks the greeting template to speak: the office's custom wording as-is if + * they've edited it (we can't auto-translate free text they wrote), otherwise + * the default translated into the patient's language when available. */ +function resolveGreetingTemplate(rawTemplate: string, defaultTemplate: string, translations: Record, language: string): string { + if (rawTemplate.trim() !== defaultTemplate) return rawTemplate; + return translations[language] ?? defaultTemplate; +} + const ENGLISH_LOCALE: VoiceLocale = { gatherLang: "en-US", sayVoice: "Polly.Joanna" }; function aiVoiceTwiml(sayText: string, gatherActionUrl: string, locale: VoiceLocale): string { @@ -738,8 +758,11 @@ router.post("/webhook/ai-voice", async (req: Request, res: Response): Promise) || {}; return { - greeting: all["_ai_call_greeting"] ?? all["_ai_chat_call_template"] ?? - "Hi {firstName}, this is Lisa, the AI assistant at {officeName}. How can I help you today?", + greeting: all["_ai_call_greeting"] ?? all["_ai_chat_call_template"] ?? DEFAULT_GREETING, reminderGreeting: all["_ai_call_reminder_greeting"] ?? "Sure, I can help with that.", newPatientGreeting: all["_ai_call_new_patient_greeting"] ?? "No problem, let's get you set up.", generalFallback: all["_ai_call_general_fallback"] ?? "Sure, how can I help?", - recallGreeting: all["_ai_call_recall_greeting"] ?? - "Hi {firstName}, this is Lisa from {officeName}. We wanted to check in — you're due for a routine exam and cleaning, which most insurance plans cover at no cost. Would you like to schedule one?", + recallGreeting: all["_ai_call_recall_greeting"] ?? DEFAULT_RECALL_GREETING, }; }, diff --git a/apps/Frontend/src/components/patients/patient-form.tsx b/apps/Frontend/src/components/patients/patient-form.tsx index e9250afd..99c6b6bd 100755 --- a/apps/Frontend/src/components/patients/patient-form.tsx +++ b/apps/Frontend/src/components/patients/patient-form.tsx @@ -1,6 +1,7 @@ import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useAuth } from "@/hooks/use-auth"; +import { useToast } from "@/hooks/use-toast"; import { Form, FormControl, @@ -50,6 +51,7 @@ export type PatientFormRef = { export const PatientForm = forwardRef( ({ patient, extractedInfo, onSubmit }, ref) => { const { user } = useAuth(); + const { toast } = useToast(); const isEditing = !!patient; const schema = useMemo( @@ -83,7 +85,7 @@ export const PatientForm = forwardRef( const computedDefaultValues = useMemo(() => { if (isEditing && patient) { - const { id, userId, createdAt, ...sanitizedPatient } = patient; + const { id, userId, createdAt, updatedAt, ...sanitizedPatient } = patient; return { ...sanitizedPatient, dateOfBirth: patient.dateOfBirth @@ -137,7 +139,7 @@ export const PatientForm = forwardRef( useEffect(() => { if (patient) { - const { id, userId, createdAt, ...sanitizedPatient } = patient; + const { id, userId, createdAt, updatedAt, ...sanitizedPatient } = patient; const normalized = normalizeInsuranceProvider(patient.insuranceProvider); const resetValues: Partial = { ...sanitizedPatient, @@ -164,9 +166,19 @@ export const PatientForm = forwardRef( id="patient-form" key={patient?.id || "new"} noValidate - onSubmit={form.handleSubmit((data) => { - handleSubmit2(data); - })} + onSubmit={form.handleSubmit( + (data) => { + handleSubmit2(data); + }, + (errors) => { + console.log("❌ Form validation errors:", errors); + toast({ + title: "Could not save patient", + description: "Please check the highlighted fields and try again.", + variant: "destructive", + }); + }, + )} className="space-y-6" > {/* Personal Information */} diff --git a/packages/db/usedSchemas/browser.ts b/packages/db/usedSchemas/browser.ts index 3d140a39..84ac1ee4 100644 --- a/packages/db/usedSchemas/browser.ts +++ b/packages/db/usedSchemas/browser.ts @@ -2,7 +2,7 @@ import * as z from "zod"; // ─── Enums ──────────────────────────────────────────────────────────────────── -export const PatientStatusSchema = z.enum(["ACTIVE", "INACTIVE", "UNKNOWN"]); +export const PatientStatusSchema = z.enum(["ACTIVE", "INACTIVE", "UNKNOWN", "PLAN_NOT_ACCEPTED"]); export type PatientStatus = z.infer; export const ClaimStatusSchema = z.enum([ @@ -85,13 +85,23 @@ export const PatientUncheckedCreateInputObjectSchema = z.object({ firstName: z.string(), lastName: z.string(), dateOfBirth: z.string(), + gender: z.string().optional(), phone: z.string().optional(), email: z.string().optional(), address: z.string().optional(), city: z.string().optional(), state: z.string().optional(), zipCode: z.string().optional(), + insuranceProvider: z.string().optional().nullable(), + insuranceId: z.string().optional().nullable(), + groupNumber: z.string().optional().nullable(), + policyHolder: z.string().optional().nullable(), + allergies: z.string().optional().nullable(), + medicalConditions: z.string().optional().nullable(), + preferredLanguage: z.string().optional().nullable(), status: PatientStatusSchema.optional(), + userId: z.number().int().optional(), + createdAt: z.coerce.date().optional(), }); export const StaffUncheckedCreateInputObjectSchema = z.object({