diff --git a/apps/Backend/src/ai/internal-chat-graph.ts b/apps/Backend/src/ai/internal-chat-graph.ts index 2d583ca2..e5fe65dd 100644 --- a/apps/Backend/src/ai/internal-chat-graph.ts +++ b/apps/Backend/src/ai/internal-chat-graph.ts @@ -59,9 +59,14 @@ Intents: e.g. "check Maria Jesus", "verify insurance for John Smith" - eligibility_by_id : user provides a member ID and date of birth (no patient name) e.g. "check masshealth for 100xxxx, 10/10/1988" -- check_and_claim : user wants to check eligibility AND submit procedures as claims + ALSO use this when user wants to check eligibility AND schedule/add an appointment on a date + e.g. "check mh for 100xxxx, 10/10/1988 and schedule on 4/10/2026" + e.g. "check mh for 100xxxx, 10/10/1988 and make appointment on 5/1/2026" + In these cases set appointmentDate to the mentioned date (YYYY-MM-DD) +- check_and_claim : user wants to check eligibility AND submit PROCEDURES/BILLING as claims e.g. "check masshealth for 100xxxx, 10/10/1988 and claim perio exam and adult cleaning" e.g. "check Maria Jesus and claim D0120 D1110" + Only use this when procedures or CDT codes are mentioned — NOT for scheduling - find_patient : look up a patient record only, no eligibility e.g. "find patient John", "look up Smith" - schedule_appointment : add a patient to the schedule (today or a specified date/time) @@ -84,6 +89,11 @@ Intents: Rules: - For check_and_claim and claim_only, procedureNames should be the RAW user text (e.g. "perio exam", "adult cleaning", "D0120") — do NOT translate to codes +- IMPORTANT: If the user says "with the x ray", "with x ray", "attach x ray", "the x ray", "with xray", + "with the attachment", "the attachment", "with attachment", "with the file", "with the image", + "with the scan", "with the photo", "with the document", or any variation meaning an uploaded/attached file, + do NOT include it in procedureNames. It refers to a file attachment, not a billable procedure. + Only include actual clinical procedures in procedureNames. - For composite fillings with a tooth number, preserve the EXACT notation including tooth# and surfaces: e.g. "composite #29 O", "#8 MO", "composite #11 MOD" — keep the #number and surface letters together as one entry - For RCT/root canal with a tooth number, preserve the tooth# in the entry: diff --git a/apps/Backend/src/ai/internal-chat-workflow.ts b/apps/Backend/src/ai/internal-chat-workflow.ts index f00f9bca..dc10d70c 100644 --- a/apps/Backend/src/ai/internal-chat-workflow.ts +++ b/apps/Backend/src/ai/internal-chat-workflow.ts @@ -11,6 +11,13 @@ import { ChatClassification } from "./internal-chat-graph"; import { lookupCdtCodes } from "./cdt-lookup"; import insuranceAliases from "../data/insuranceAliases.json"; +// Phrases the user may write to mean "attach the uploaded file" — not CDT procedures +const ATTACHMENT_PHRASES = /^(with\s+)?(the\s+)?(x[\s-]?ray[s]?|xray[s]?|radiograph[s]?|image[s]?|film[s]?|attachment[s]?|attach|file[s]?|photo[s]?|picture[s]?|scan[s]?|doc(ument)?[s]?)$/i; + +function stripAttachmentRefs(names: string[]): string[] { + return names.filter((n) => !ATTACHMENT_PHRASES.test(n.trim())); +} + // ─── Types ──────────────────────────────────────────────────────────────────── export interface ResolvedPatient { @@ -358,6 +365,7 @@ async function handleEligibilityById( dob: resolvedDob, siteKey, autoCheck: siteKeyToAutoCheck(siteKey), + appointmentDate: c.appointmentDate ?? null, }, }; } @@ -423,7 +431,7 @@ async function handleCheckAndClaim( } // 3. Map procedure names → CDT codes (custom aliases take priority) - const procedureNames = c.procedureNames ?? []; + const procedureNames = stripAttachmentRefs(c.procedureNames ?? []); const cdtResults: CdtResult[] = procedureNames.length > 0 ? lookupCdtCodes(procedureNames, customAliases) : []; @@ -492,7 +500,7 @@ async function handleClaimOnly( const fullName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim(); // Map procedure names → CDT codes - const procedureNames = c.procedureNames ?? []; + const procedureNames = stripAttachmentRefs(c.procedureNames ?? []); if (procedureNames.length === 0) { return { reply: "Please specify which procedures to claim (e.g. perio exam, adult prophy)." }; } @@ -741,15 +749,16 @@ async function handleScheduleAppointment( export async function createAppointmentToday( patientId: number, userId: number, - storage: StorageLike + storage: StorageLike, + targetDate?: string // YYYY-MM-DD; defaults to today ): Promise<{ startTime: string; endTime: string; dateStr: string; dateLabel: string; column: string } | { error: string }> { - const today = new Date(); - const dateStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`; - const dateLabel = today.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }); - const localDate = new Date(today.getFullYear(), today.getMonth(), today.getDate()); + const base = targetDate ? new Date(targetDate + "T00:00:00") : new Date(); + const dateStr = `${base.getFullYear()}-${String(base.getMonth() + 1).padStart(2, "0")}-${String(base.getDate()).padStart(2, "0")}`; + const dateLabel = base.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }); + const localDate = new Date(base.getFullYear(), base.getMonth(), base.getDate()); const dayNames = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"]; - const dayName = dayNames[today.getDay()]!; + const dayName = dayNames[base.getDay()]!; const officeHours = await storage.getOfficeHours(userId); const dayHours = officeHours?.data?.doctors?.[dayName] ?? { @@ -757,7 +766,7 @@ export async function createAppointmentToday( }; if (!dayHours.enabled) { - return { error: `The office is closed today (${dayName}). Cannot create appointment.` }; + return { error: `The office is closed on ${dayName} (${dateLabel}). Cannot create appointment.` }; } const allSlots = buildSlots(dayHours); diff --git a/apps/Backend/src/cron/backupCheck.ts b/apps/Backend/src/cron/backupCheck.ts index 4c28c540..40d99b44 100755 --- a/apps/Backend/src/cron/backupCheck.ts +++ b/apps/Backend/src/cron/backupCheck.ts @@ -78,7 +78,7 @@ export const startBackupCron = () => { const log = await cronJobLogStorage.createJobLog("local-backup", startedAt); try { - const filename = `dental_backup_${Date.now()}.sql`; + const filename = `dental_backup_${Date.now()}.zip`; await backupDatabaseToPath({ destinationPath: LOCAL_BACKUP_DIR, filename }); pruneOldBackups(LOCAL_BACKUP_DIR); await storage.createBackup(admin.id); @@ -153,7 +153,7 @@ export const startBackupCron = () => { } try { - const filename = `dental_backup_usb_${Date.now()}.sql`; + const filename = `dental_backup_usb_${Date.now()}.zip`; await backupDatabaseToPath({ destinationPath: usbBackupPath, filename }); pruneOldBackups(usbBackupPath); await storage.createBackup(admin.id); diff --git a/apps/Backend/src/routes/ai-settings.ts b/apps/Backend/src/routes/ai-settings.ts index b3baa037..48379939 100644 --- a/apps/Backend/src/routes/ai-settings.ts +++ b/apps/Backend/src/routes/ai-settings.ts @@ -313,9 +313,9 @@ router.post("/create-appointment-today", async (req: Request, res: Response): Pr try { const userId = req.user?.id; if (!userId) return res.status(401).json({ message: "Unauthorized" }); - const { patientId } = req.body; + const { patientId, date } = req.body; if (!patientId) return res.status(400).json({ message: "patientId is required" }); - const result = await createAppointmentToday(Number(patientId), userId, storage); + const result = await createAppointmentToday(Number(patientId), userId, storage, date ?? undefined); if ("error" in result) return res.status(409).json({ message: result.error }); return res.status(200).json(result); } catch (err) { diff --git a/apps/Backend/src/routes/claims.ts b/apps/Backend/src/routes/claims.ts index 26a74c6d..25aea799 100755 --- a/apps/Backend/src/routes/claims.ts +++ b/apps/Backend/src/routes/claims.ts @@ -90,45 +90,66 @@ const upload = multer({ }, }); -// Disk-storage uploader for claim attachments saved under uploads/patients// -const attachmentDiskStorage = multer.diskStorage({ - destination: (req, _file, cb) => { - const patientName = String(req.body.patientName || "unknown").replace(/[/\\?%*:|"<>]/g, "-").trim(); - const dir = path.join(process.cwd(), "uploads", "patients", patientName); - fs.mkdirSync(dir, { recursive: true }); - cb(null, dir); - }, - filename: (_req, file, cb) => { - const ext = path.extname(file.originalname); - const base = path.basename(file.originalname, ext).replace(/\s+/g, "_"); - cb(null, `${Date.now()}_${base}${ext}`); - }, -}); -const attachmentUpload = multer({ - storage: attachmentDiskStorage, - limits: { fileSize: 20 * 1024 * 1024 }, - fileFilter: (_req, file, cb) => { - const allowed = ["application/pdf", "image/jpeg", "image/png", "image/webp"]; - cb(null, allowed.includes(file.mimetype)); - }, -}); -// POST /api/claims/upload-attachments -// Saves files to uploads/patients// and returns their paths. +// POST /api/claims/upload-to-cloud +// Saves uploaded files to both: +// - Patient Documents (visible on Documents page) +// - Cloud Storage patient folder (visible on Cloud Storage page) router.post( - "/upload-attachments", - attachmentUpload.array("files", 10), - (req: Request, res: Response): any => { + "/upload-to-cloud", + upload.array("files", 10), + async (req: Request, res: Response): Promise => { + if (!req.user?.id) return res.status(401).json({ error: "Unauthorized" }); + const files = req.files as Express.Multer.File[]; - if (!files?.length) return res.status(400).json({ error: true, message: "No files uploaded" }); + if (!files?.length) return res.status(400).json({ error: "No files uploaded" }); - const result = files.map((f) => ({ - filename: f.originalname, - mimeType: f.mimetype, - filePath: `/uploads/patients/${path.basename(path.dirname(f.path))}/${path.basename(f.path)}`, - })); + const patientId = Number(req.body.patientId); + const patientName = String(req.body.patientName || "unknown").replace(/[/\\?%*:|"<>]/g, "-").trim(); - return res.json({ error: false, data: result }); + if (!patientId || isNaN(patientId)) { + return res.status(400).json({ error: "Invalid patientId" }); + } + + try { + const folder = await storage.getOrCreatePatientFolder(req.user.id, patientId, patientName); + const result: { filename: string; mimeType: string; filePath: string }[] = []; + + for (const file of files) { + // Save to patient-documents (Documents page) + await storage.createPatientDocument( + patientId, + file.originalname, + file.originalname, + file.mimetype, + file.size, + file.buffer + ); + + // Save to cloud storage patient folder (Cloud Storage page) + const cloudFile = await storage.initializeFileUpload( + req.user.id, + file.originalname, + file.mimetype, + BigInt(file.size), + 1, + (folder as any).id + ); + await storage.appendFileChunk((cloudFile as any).id, 0, file.buffer); + await storage.finalizeFileUpload((cloudFile as any).id); + + result.push({ + filename: file.originalname, + mimeType: file.mimetype, + filePath: `/api/cloud-storage/files/${(cloudFile as any).id}/content`, + }); + } + + return res.json({ error: false, data: result }); + } catch (err: any) { + console.error("[upload-to-cloud]", err); + return res.status(500).json({ error: "Failed to upload files", message: err?.message }); + } } ); diff --git a/apps/Backend/src/routes/database-management.ts b/apps/Backend/src/routes/database-management.ts index 41c251e9..dce351a5 100755 --- a/apps/Backend/src/routes/database-management.ts +++ b/apps/Backend/src/routes/database-management.ts @@ -370,7 +370,7 @@ router.post("/backup-path", async (req, res) => { }); } - const filename = `dental_backup_${Date.now()}.sql`; + const filename = `dental_backup_${Date.now()}.zip`; try { await backupDatabaseToPath({ diff --git a/apps/Frontend/src/components/claims/claim-form.tsx b/apps/Frontend/src/components/claims/claim-form.tsx index 5d483c11..741118a4 100755 --- a/apps/Frontend/src/components/claims/claim-form.tsx +++ b/apps/Frontend/src/components/claims/claim-form.tsx @@ -899,11 +899,10 @@ export function ClaimForm({ ...formToCreateClaim } = f; - // build claimFiles metadata from uploadedFiles (only filename + mimeType) - const claimFilesMeta: ClaimFileMeta[] = (uploadedFiles || []).map((f) => ({ - filename: f.name, - mimeType: f.type, - })); + // Save uploaded files to disk (uploads/patients//) and record their paths + const claimFilesMeta: ClaimFileMeta[] = uploadedFiles?.length + ? await uploadAttachmentsToLocalFolder(uploadedFiles) + : []; const selectedNpiProviderId = npiProvider?.npiNumber ? npiProviders.find((p) => p.npiNumber === npiProvider.npiNumber)?.id ?? null @@ -1423,10 +1422,11 @@ export function ClaimForm({ : patient?.firstName ?? `patient-${patientId}`; const formData = new FormData(); + formData.append("patientId", String(patientId)); formData.append("patientName", patientName); files.forEach((f) => formData.append("files", f)); - const res = await apiRequest("POST", "/api/claims/upload-attachments", formData); + const res = await apiRequest("POST", "/api/claims/upload-to-cloud", formData); const data = await res.json(); return (data.data ?? []) as ClaimFileMeta[]; }; diff --git a/apps/Frontend/src/components/layout/chatbot.tsx b/apps/Frontend/src/components/layout/chatbot.tsx index f6af3fde..4b887399 100644 --- a/apps/Frontend/src/components/layout/chatbot.tsx +++ b/apps/Frontend/src/components/layout/chatbot.tsx @@ -135,7 +135,7 @@ export function ChatbotButton() { const [eligibilityData, setEligibilityData] = useState(null); const [freeTextInput, setFreeTextInput] = useState(""); const [patientResult, setPatientResult] = useState(null); - const [eligibilityIdData, setEligibilityIdData] = useState<{ memberId: string; dob: string; siteKey: string; autoCheck: string; patient: PatientResult | null } | null>(null); + const [eligibilityIdData, setEligibilityIdData] = useState<{ memberId: string; dob: string; siteKey: string; autoCheck: string; patient: PatientResult | null; appointmentDate?: string | null } | null>(null); const [checkAndClaimData, setCheckAndClaimData] = useState(null); const [clarificationData, setClarificationData] = useState<{ memberId: string; dob: string; patient: PatientResult | null; procedureNames: string[]; options: string[] } | null>(null); const [apptSelectionData, setApptSelectionData] = useState<{ @@ -299,29 +299,32 @@ export function ChatbotButton() { prefillAndNavigate(eligibilityIdData.memberId, eligibilityIdData.dob, eligibilityIdData.autoCheck); }; - const handleEligibilityAndAppointment = async () => { + const handleEligibilityAndAppointment = async (targetDate?: string) => { if (!eligibilityIdData) return; - addMsg("user", "Check eligibility & add to today's schedule"); + const dateLabel = targetDate + ? new Date(targetDate + "T00:00:00").toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }) + : "today"; + addMsg("user", `Check eligibility & add to schedule (${dateLabel})`); if (!eligibilityIdData.patient) { - // Patient not yet in DB — eligibility check will create them; flag for post-check appointment - addMsg("bot", "Running eligibility check — will add patient and create today's appointment after..."); - sessionStorage.setItem("chatbot_appt_after_eligibility", JSON.stringify({ memberId: eligibilityIdData.memberId })); + addMsg("bot", `Running eligibility check — will add patient and create appointment for ${dateLabel} after...`); + sessionStorage.setItem("chatbot_appt_after_eligibility", JSON.stringify({ memberId: eligibilityIdData.memberId, date: targetDate ?? null })); prefillAndNavigate(eligibilityIdData.memberId, eligibilityIdData.dob, eligibilityIdData.autoCheck); return; } - addMsg("bot", "Creating appointment for today...", true); + addMsg("bot", `Creating appointment for ${dateLabel}...`, true); try { const res = await apiRequest("POST", "/api/ai/create-appointment-today", { patientId: eligibilityIdData.patient.id, + date: targetDate ?? undefined, }); const data = await res.json(); if (!res.ok) { replaceLastMsg(data.message ?? "Could not create appointment."); return; } - replaceLastMsg(`Appointment added at ${data.startTime} (${data.column ?? "Column A"}) — opening eligibility check page...`); + replaceLastMsg(`Appointment added at ${data.startTime} (${data.column ?? "Column A"}) for ${data.dateLabel} — opening eligibility check page...`); prefillAndNavigate(eligibilityIdData.memberId, eligibilityIdData.dob, eligibilityIdData.autoCheck); } catch { replaceLastMsg("Could not create appointment. Please try again."); @@ -393,6 +396,7 @@ export function ChatbotButton() { siteKey: data.actionData.siteKey, autoCheck: data.actionData.autoCheck, patient: data.actionData.patient ?? null, + appointmentDate: data.actionData.appointmentDate ?? null, }); setStep("eligibility-id-ready"); return; @@ -694,11 +698,22 @@ export function ChatbotButton() { + {eligibilityIdData.appointmentDate && ( + + )} @@ -780,6 +795,7 @@ export function ChatbotButton() { siteKey: data.actionData.siteKey, autoCheck: data.actionData.autoCheck, patient: data.actionData.patient ?? null, + appointmentDate: data.actionData.appointmentDate ?? null, }); setStep("eligibility-id-ready"); } else { diff --git a/apps/Frontend/src/pages/appointments-page.tsx b/apps/Frontend/src/pages/appointments-page.tsx index 418210ee..eed95b01 100755 --- a/apps/Frontend/src/pages/appointments-page.tsx +++ b/apps/Frontend/src/pages/appointments-page.tsx @@ -181,6 +181,16 @@ export default function AppointmentsPage() { reply: string; } | null>(null); const [needsAiClaimResume, setNeedsAiClaimResume] = useState(false); + const [aiClaimCdtClarification, setAiClaimCdtClarification] = useState<{ + unknownPhrases: string[]; + codeInputs: Record; + originalMessage: string; + aptDate: string; + appointmentId: number; + patientName: string; + notes: string; + matchedSoFar: Array<{ code: string; description: string }>; + } | null>(null); const [selectedReminderColumns, setSelectedReminderColumns] = useState>(new Set()); const [isSendingReminders, setIsSendingReminders] = useState(false); const [reminderAiFollowUp, setReminderAiFollowUp] = useState(true); @@ -1413,6 +1423,20 @@ export default function AppointmentsPage() { notes: apt.notes ?? "", reply: data.reply ?? "", }); + } else if (data.action === "need_cdt_clarification" && data.actionData) { + const phrases: string[] = data.actionData.unknownPhrases ?? []; + const inputs: Record = {}; + for (const p of phrases) inputs[p] = ""; + setAiClaimCdtClarification({ + unknownPhrases: phrases, + codeInputs: inputs, + originalMessage: `claim ${apt.notes} for ${apt.patientName}`, + aptDate, + appointmentId: Number(apt.id), + patientName: apt.patientName, + notes: apt.notes ?? "", + matchedSoFar: data.actionData.matchedSoFar ?? [], + }); } else { setAiClaimCurrentData({ matchedCodes: [], @@ -1484,11 +1508,78 @@ export default function AppointmentsPage() { }; const handleAiClaimSkip = async () => { + setAiClaimCdtClarification(null); const nextIndex = aiClaimCurrentIndex + 1; setAiClaimCurrentIndex(nextIndex); await processAiClaimAtIndex(aiClaimQueue, nextIndex); }; + const handleAiClaimCdtSubmit = async () => { + if (!aiClaimCdtClarification) return; + const { codeInputs, originalMessage, aptDate, appointmentId, patientName, notes, matchedSoFar } = aiClaimCdtClarification; + setAiClaimCdtClarification(null); + setIsAiClaimProcessing(true); + setAiClaimCurrentData(null); + try { + for (const [phrase, code] of Object.entries(codeInputs)) { + await apiRequest("POST", "/api/ai/cdt-aliases/add", { phrase, cdtCode: code.trim() }); + } + const res = await apiRequest("POST", "/api/ai/internal-chat", { + message: originalMessage, + clientDate: aptDate, + }); + const data = await res.json(); + if ((data.action === "claim_only_ready" || data.action === "check_and_claim_ready") && data.actionData) { + setAiClaimCurrentData({ + matchedCodes: data.actionData.matchedCodes ?? [], + siteKey: data.actionData.siteKey ?? "", + serviceDate: data.actionData.serviceDate ?? aptDate, + appointmentId, + patientName, + notes, + reply: data.reply ?? "", + }); + } else if (data.action === "need_cdt_clarification" && data.actionData) { + // Still has unknowns — loop again + const phrases: string[] = data.actionData.unknownPhrases ?? []; + const inputs: Record = {}; + for (const p of phrases) inputs[p] = ""; + setAiClaimCdtClarification({ + unknownPhrases: phrases, + codeInputs: inputs, + originalMessage, + aptDate, + appointmentId, + patientName, + notes, + matchedSoFar: data.actionData.matchedSoFar ?? matchedSoFar, + }); + } else { + setAiClaimCurrentData({ + matchedCodes: matchedSoFar.map((c) => ({ ...c, toothNumber: undefined, toothSurface: undefined })), + siteKey: "", + serviceDate: aptDate, + appointmentId, + patientName, + notes, + reply: data.reply ?? "Could not fully interpret notes.", + }); + } + } catch { + setAiClaimCurrentData({ + matchedCodes: matchedSoFar.map((c) => ({ ...c, toothNumber: undefined, toothSurface: undefined })), + siteKey: "", + serviceDate: aptDate, + appointmentId, + patientName, + notes, + reply: "Error retrying after alias save.", + }); + } finally { + setIsAiClaimProcessing(false); + } + }; + return (
Interpreting notes with AI...
+ ) : aiClaimCdtClarification ? ( +
+
+

{aiClaimCdtClarification.patientName}

+

+ Notes: {aiClaimCdtClarification.notes} +

+
+

+ Unknown term{aiClaimCdtClarification.unknownPhrases.length > 1 ? "s" : ""} — enter CDT code{aiClaimCdtClarification.unknownPhrases.length > 1 ? "s" : ""}: +

+ {aiClaimCdtClarification.matchedSoFar.length > 0 && ( +
+

Already matched

+ {aiClaimCdtClarification.matchedSoFar.map((c) => ( +

+ {c.code} + — {c.description} +

+ ))} +
+ )} +
+ {aiClaimCdtClarification.unknownPhrases.map((phrase) => ( +
+ "{phrase}" → + + setAiClaimCdtClarification((prev) => + prev ? { ...prev, codeInputs: { ...prev.codeInputs, [phrase]: e.target.value.toUpperCase() } } : prev + ) + } + className="flex-1 rounded border border-amber-300 bg-white px-2 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-amber-400" + /> +
+ ))} +
+
+ + + +
+
) : aiClaimCurrentData ? (
diff --git a/apps/Frontend/src/pages/insurance-status-page.tsx b/apps/Frontend/src/pages/insurance-status-page.tsx index fd2fbbaf..47543ca1 100755 --- a/apps/Frontend/src/pages/insurance-status-page.tsx +++ b/apps/Frontend/src/pages/insurance-status-page.tsx @@ -720,7 +720,7 @@ export default function InsuranceStatusPage() { try { const raw = sessionStorage.getItem("chatbot_appt_after_eligibility"); if (!raw) return; - const { memberId: storedMemberId } = JSON.parse(raw); + const { memberId: storedMemberId, date: storedDate } = JSON.parse(raw); sessionStorage.removeItem("chatbot_appt_after_eligibility"); if (!storedMemberId) return; @@ -729,12 +729,15 @@ export default function InsuranceStatusPage() { const patient = await lookupRes.json(); if (!patient?.id) return; - const apptRes = await apiRequest("POST", "/api/ai/create-appointment-today", { patientId: patient.id }); + const apptRes = await apiRequest("POST", "/api/ai/create-appointment-today", { patientId: patient.id, date: storedDate ?? undefined }); const apptData = await apptRes.json(); if (apptRes.ok) { + const scheduledOn = storedDate + ? new Date(storedDate + "T00:00:00").toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }) + : "today"; toast({ title: "Appointment created", - description: `${patient.firstName ?? ""} ${patient.lastName ?? ""} added to today's schedule at ${apptData.startTime} (${apptData.column ?? "Column A"}).`.trim(), + description: `${patient.firstName ?? ""} ${patient.lastName ?? ""} added to schedule (${scheduledOn}) at ${apptData.startTime} (${apptData.column ?? "Column A"}).`.trim(), }); } else { toast({