From b20dc8e97678d6f21574f2fe369d1626f8c75443 Mon Sep 17 00:00:00 2001 From: ff Date: Thu, 28 May 2026 15:39:36 -0400 Subject: [PATCH] fix: D0140 claim sync, schedule column prefill, multi-appt, DentaQuest OTP session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix Express route ordering in appointments-procedures so /prefill-from-appointment is matched before /:id (D0140 and other codes now always reach the claim) - claim-form: always fetch AppointmentProcedure records when an existing claim loads so post-save procedure edits (e.g. adding D0140) are reflected immediately - appointments-page: replace sessionStorage with React state (newApptPrefill) for slot-click prefill so columns B-F correctly carry their staffId into the form - add-appointment-modal / appointment-form: thread prefillData prop; add NewAppointmentPrefill interface; useEffect applies values via setValue - appointments upsert: remove per-patient dedup so the same patient can have multiple appointments on the same day in the same column - DentaQuest / TuftsSCO: navigate to about:blank and minimize instead of quit_driver after each run — session cookie stays in memory so OTP is only required once per app startup, not on every eligibility or claim check Co-Authored-By: Claude Sonnet 4.6 --- .../src/routes/appointments-procedures.ts | 80 +++++------ apps/Backend/src/routes/appointments.ts | 46 ++----- .../appointments/add-appointment-modal.tsx | 5 +- .../appointments/appointment-form.tsx | 128 ++++++------------ .../src/components/claims/claim-form.tsx | 62 ++++++--- apps/Frontend/src/pages/appointments-page.tsx | 106 +++------------ .../SeleniumService/helpers_tuftssco_claim.py | 17 ++- ...enium_DentaQuest_eligibilityCheckWorker.py | 18 ++- 8 files changed, 181 insertions(+), 281 deletions(-) diff --git a/apps/Backend/src/routes/appointments-procedures.ts b/apps/Backend/src/routes/appointments-procedures.ts index ad82d572..cf13ae12 100755 --- a/apps/Backend/src/routes/appointments-procedures.ts +++ b/apps/Backend/src/routes/appointments-procedures.ts @@ -8,25 +8,7 @@ import { const router = Router(); -/** - * GET /api/appointment-procedures/:appointmentId - * Get all procedures for an appointment - */ -router.get("/:appointmentId", async (req: Request, res: Response) => { - try { - const appointmentId = Number(req.params.appointmentId); - if (isNaN(appointmentId)) { - return res.status(400).json({ message: "Invalid appointmentId" }); - } - - const rows = await storage.getByAppointmentId(appointmentId); - - return res.json(rows); - } catch (err: any) { - console.error("GET appointment procedures error", err); - return res.status(500).json({ message: err.message ?? "Server error" }); - } -}); +// Specific routes must be registered before generic /:id routes to avoid shadowing. router.get( "/prefill-from-appointment/:appointmentId", @@ -54,6 +36,26 @@ router.get( } ); +/** + * GET /api/appointment-procedures/:appointmentId + * Get all procedures for an appointment + */ +router.get("/:appointmentId", async (req: Request, res: Response) => { + try { + const appointmentId = Number(req.params.appointmentId); + if (isNaN(appointmentId)) { + return res.status(400).json({ message: "Invalid appointmentId" }); + } + + const rows = await storage.getByAppointmentId(appointmentId); + + return res.json(rows); + } catch (err: any) { + console.error("GET appointment procedures error", err); + return res.status(500).json({ message: err.message ?? "Server error" }); + } +}); + /** * PUT /api/appointment-procedures/set-npi-provider/:appointmentId * Set the npiProviderId on all procedures for an appointment (lightweight update). @@ -196,26 +198,6 @@ router.put("/:id", async (req: Request, res: Response) => { } }); -/** - * DELETE /api/appointment-procedures/:id - * Delete single procedure - */ -router.delete("/:id", async (req: Request, res: Response) => { - try { - const id = Number(req.params.id); - if (isNaN(id)) { - return res.status(400).json({ message: "Invalid id" }); - } - - await storage.deleteProcedure(id); - - return res.json({ success: true }); - } catch (err: any) { - console.error("DELETE appointment procedure error", err); - return res.status(500).json({ message: err.message ?? "Server error" }); - } -}); - /** * DELETE /api/appointment-procedures/clear/:appointmentId * Clear all procedures for appointment @@ -236,4 +218,24 @@ router.delete("/clear/:appointmentId", async (req: Request, res: Response) => { } }); +/** + * DELETE /api/appointment-procedures/:id + * Delete single procedure + */ +router.delete("/:id", async (req: Request, res: Response) => { + try { + const id = Number(req.params.id); + if (isNaN(id)) { + return res.status(400).json({ message: "Invalid id" }); + } + + await storage.deleteProcedure(id); + + return res.json({ success: true }); + } catch (err: any) { + console.error("DELETE appointment procedure error", err); + return res.status(500).json({ message: err.message ?? "Server error" }); + } +}); + export default router; diff --git a/apps/Backend/src/routes/appointments.ts b/apps/Backend/src/routes/appointments.ts index a20563aa..0bc6b4f5 100755 --- a/apps/Backend/src/routes/appointments.ts +++ b/apps/Backend/src/routes/appointments.ts @@ -193,20 +193,13 @@ router.post( const originalStartTime = appointmentData.startTime; const MAX_END_TIME = "18:30"; - // 1. Verify patient exists and belongs to user + // 1. Verify patient exists const patient = await storage.getPatient(appointmentData.patientId); if (!patient) { return res.status(404).json({ message: "Patient not found" }); } - // 2. One patient per column per day: find existing appointment for this patient in the same staff column today - const existingPatientAppointment = await storage.getPatientAppointmentByDateAndStaff( - appointmentData.patientId, - appointmentData.date, - appointmentData.staffId - ); - - // 3. Attempt to find the next available slot + // 2. Find the next available slot for this staff member (multiple same-patient same-day bookings allowed) let [hour, minute] = originalStartTime.split(":").map(Number); const pad = (n: number) => n.toString().padStart(2, "0"); @@ -216,45 +209,24 @@ router.post( while (`${pad(hour)}:${pad(minute)}` <= MAX_END_TIME) { const currentStartTime = `${pad(hour)}:${pad(minute)}`; - // Check staff conflict at this time (exclude the patient's existing appointment so it can move) const staffConflict = await storage.getStaffAppointmentByDateTime( appointmentData.staffId, appointmentData.date, currentStartTime, - existingPatientAppointment?.id + undefined ); if (!staffConflict) { const endMinute = minute + APPT_DURATION_MINUTES; - let endHour = hour + Math.floor(endMinute / 60); - let realEndMinute = endMinute % 60; - + const endHour = hour + Math.floor(endMinute / 60); + const realEndMinute = endMinute % 60; const currentEndTime = `${pad(endHour)}:${pad(realEndMinute)}`; - const payload = { + const created = await storage.createAppointment({ ...appointmentData, startTime: currentStartTime, endTime: currentEndTime, - }; - - if (existingPatientAppointment?.id !== undefined) { - // Replace the existing appointment in-place (preserves linked claims/procedures) - const updated = await storage.updateAppointment( - existingPatientAppointment.id, - payload - ); - return res.status(200).json({ - ...updated, - originalRequestedTime: originalStartTime, - finalScheduledTime: currentStartTime, - message: - originalStartTime !== currentStartTime - ? `Your requested time (${originalStartTime}) was unavailable. Appointment was updated to ${currentStartTime}.` - : `Appointment successfully updated at ${currentStartTime}.`, - }); - } - - const created = await storage.createAppointment(payload); + }); return res.status(201).json({ ...created, originalRequestedTime: originalStartTime, @@ -266,7 +238,6 @@ router.post( }); } - // Move to next STEP_MINUTES slot minute += STEP_MINUTES; if (minute >= 60) { hour += Math.floor(minute / 60); @@ -275,8 +246,7 @@ router.post( } return res.status(409).json({ - message: - "No available slots remaining until 6:30 PM for this Staff. Please choose another day.", + message: "No available slots remaining until 6:30 PM for this staff member. Please choose another day.", }); } catch (error) { console.error("Error in upsert appointment:", error); diff --git a/apps/Frontend/src/components/appointments/add-appointment-modal.tsx b/apps/Frontend/src/components/appointments/add-appointment-modal.tsx index ec9f6416..fcd6e193 100755 --- a/apps/Frontend/src/components/appointments/add-appointment-modal.tsx +++ b/apps/Frontend/src/components/appointments/add-appointment-modal.tsx @@ -4,7 +4,7 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { AppointmentForm } from "./appointment-form"; +import { AppointmentForm, type NewAppointmentPrefill } from "./appointment-form"; import { Appointment, InsertAppointment, @@ -18,6 +18,7 @@ interface AddAppointmentModalProps { onDelete?: (id: number) => void; isLoading: boolean; appointment?: Appointment; + prefillData?: NewAppointmentPrefill | null; } export function AddAppointmentModal({ @@ -27,6 +28,7 @@ export function AddAppointmentModal({ onDelete, isLoading, appointment, + prefillData, }: AddAppointmentModalProps) { return ( @@ -39,6 +41,7 @@ export function AddAppointmentModal({
{ onSubmit(data); onOpenChange(false); diff --git a/apps/Frontend/src/components/appointments/appointment-form.tsx b/apps/Frontend/src/components/appointments/appointment-form.tsx index 8fc1b8f7..ae925355 100755 --- a/apps/Frontend/src/components/appointments/appointment-form.tsx +++ b/apps/Frontend/src/components/appointments/appointment-form.tsx @@ -37,8 +37,18 @@ import { DateInputField } from "@/components/ui/dateInputField"; import { formatLocalDate, parseLocalDate } from "@/utils/dateUtils"; import { toast } from "@/hooks/use-toast"; +export interface NewAppointmentPrefill { + staffId: number; + date: string; + startTime: string; + endTime: string; + patientId?: number; + type?: string; +} + interface AppointmentFormProps { appointment?: Appointment; + prefillData?: NewAppointmentPrefill | null; onSubmit: (data: InsertAppointment | UpdateAppointment) => void; onDelete?: (id: number) => void; onOpenChange?: (open: boolean) => void; @@ -47,6 +57,7 @@ interface AppointmentFormProps { export function AppointmentForm({ appointment, + prefillData, onSubmit, onDelete, onOpenChange, @@ -87,19 +98,6 @@ export function AppointmentForm({ color: colorMap[staff.name] || "bg-gray-400", })); - // Get the stored data from session storage - const storedDataString = sessionStorage.getItem("newAppointmentData"); - let parsedStoredData = null; - - // Try to parse it if it exists - if (storedDataString) { - try { - parsedStoredData = JSON.parse(storedDataString); - } catch (error) { - console.error("Error parsing stored appointment data:", error); - } - } - // Format the date and times for the form const defaultValues: Partial = appointment ? { @@ -107,8 +105,8 @@ export function AppointmentForm({ patientId: appointment.patientId, title: appointment.title, date: parseLocalDate(appointment.date), - startTime: appointment.startTime || "09:00", // Default "09:00" - endTime: appointment.endTime || "09:30", // Default "09:30" + startTime: appointment.startTime || "09:00", + endTime: appointment.endTime || "09:30", type: appointment.type?.startsWith("other:") ? "other" : appointment.type, notes: appointment.notes || "", status: appointment.status || "scheduled", @@ -117,23 +115,18 @@ export function AppointmentForm({ ? appointment.staffId : undefined, } - : parsedStoredData + : prefillData ? { userId: user?.id, - patientId: Number(parsedStoredData.patientId), - date: parsedStoredData.date - ? parseLocalDate(parsedStoredData.date) - : parseLocalDate(new Date()), - title: parsedStoredData.title || "", - startTime: parsedStoredData.startTime, - endTime: parsedStoredData.endTime, - type: parsedStoredData.type || "checkup", - status: parsedStoredData.status || "scheduled", - notes: parsedStoredData.notes || "", - staffId: - typeof parsedStoredData.staff === "number" - ? parsedStoredData.staff - : (staffMembers?.[0]?.id ?? undefined), + patientId: prefillData.patientId, + date: prefillData.date ? parseLocalDate(prefillData.date) : new Date(), + title: "", + startTime: prefillData.startTime, + endTime: prefillData.endTime, + type: prefillData.type || "checkup", + status: "scheduled", + notes: "", + staffId: prefillData.staffId, } : { userId: user?.id ?? 0, @@ -192,67 +185,24 @@ export function AppointmentForm({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectOpen]); - // Force form field values to update and clean up storage + // Prefill form from prefillData prop (new appointment slot click) useEffect(() => { - if (!parsedStoredData) return; - - // set times/staff/date as before - if (parsedStoredData.startTime) - form.setValue("startTime", parsedStoredData.startTime); - if (parsedStoredData.endTime) - form.setValue("endTime", parsedStoredData.endTime); - if (parsedStoredData.staff) - form.setValue("staffId", parsedStoredData.staff); - if (parsedStoredData.date) { - form.setValue("date", parseLocalDate(parsedStoredData.date)); + if (!prefillData) return; + form.setValue("staffId", prefillData.staffId); + form.setValue("startTime", prefillData.startTime); + form.setValue("endTime", prefillData.endTime); + form.setValue("date", parseLocalDate(prefillData.date)); + if (prefillData.type) form.setValue("type", prefillData.type); + if (prefillData.patientId) { + form.setValue("patientId", prefillData.patientId); + (async () => { + try { + const res = await apiRequest("GET", `/api/patients/${prefillData.patientId}`); + if (res.ok) setPrefillPatient(await res.json()); + } catch {} + })(); } - - // ---- patient prefill: check main cache, else fetch once ---- - if (parsedStoredData.patientId) { - const pid = Number(parsedStoredData.patientId); - if (!Number.isNaN(pid)) { - // ensure the form value is set - form.setValue("patientId", pid); - - // fetch single patient record (preferred) - (async () => { - try { - const res = await apiRequest("GET", `/api/patients/${pid}`); - if (res.ok) { - const patientRecord = await res.json(); - setPrefillPatient(patientRecord); - } else { - // non-OK response: show toast with status / message - let msg = `Failed to load patient (status ${res.status})`; - try { - const body = await res.json().catch(() => null); - if (body && body.message) msg = body.message; - } catch {} - toast({ - title: "Could not load patient", - description: msg, - variant: "destructive", - }); - } - } catch (err) { - toast({ - title: "Error fetching patient", - description: - (err as Error)?.message || - "An unknown error occurred while fetching patient details.", - variant: "destructive", - }); - } finally { - // remove the one-time transport - sessionStorage.removeItem("newAppointmentData"); - } - })(); - } - } else { - // no patientId in storage — still remove to avoid stale state - sessionStorage.removeItem("newAppointmentData"); - } - }, [form]); + }, [prefillData]); // When editing an appointment, ensure we prefill the patient so SelectValue can render useEffect(() => { diff --git a/apps/Frontend/src/components/claims/claim-form.tsx b/apps/Frontend/src/components/claims/claim-form.tsx index c97f5c9d..a8da3568 100755 --- a/apps/Frontend/src/components/claims/claim-form.tsx +++ b/apps/Frontend/src/components/claims/claim-form.tsx @@ -331,26 +331,54 @@ export function ClaimForm({ } catch {} } - // Restore service lines - const mappedLines = (claim.serviceLines ?? []).map((sl: any) => ({ - procedureCode: sl.procedureCode ?? "", - procedureDate: sl.procedureDate - ? String(sl.procedureDate).split("T")[0] - : claimDate, - quad: sl.quad ?? "", - arch: sl.arch ?? "", - toothNumber: sl.toothNumber ?? "", - toothSurface: sl.toothSurface ?? "", - totalBilled: new Decimal(Number(sl.totalBilled ?? 0)), - totalAdjusted: new Decimal(Number(sl.totalAdjusted ?? 0)), - totalPaid: new Decimal(Number(sl.totalPaid ?? 0)), - })); + // Prefer AppointmentProcedure records for service lines — they reflect any + // updates the user made in the Select Procedure dialog after the claim was saved. + let serviceLines: InputServiceLine[] = []; + try { + const procRes = await apiRequest( + "GET", + `/api/appointment-procedures/prefill-from-appointment/${appointmentId}`, + ); + if (procRes.ok) { + const procData = await procRes.json(); + if ((procData.procedures || []).length > 0) { + serviceLines = (procData.procedures as any[]).map((p) => ({ + procedureCode: p.procedureCode ?? "", + procedureDate: claimDate || serviceDate, + quad: p.quad || "", + arch: p.arch || "", + toothNumber: p.toothNumber || "", + toothSurface: p.toothSurface || "", + totalBilled: new Decimal(Number(p.fee ?? 0)), + totalAdjusted: new Decimal(0), + totalPaid: new Decimal(0), + })); + } + } + } catch {} + + // Fall back to the claim's own service lines if no AppointmentProcedure records exist + if (serviceLines.length === 0) { + serviceLines = (claim.serviceLines ?? []).map((sl: any) => ({ + procedureCode: sl.procedureCode ?? "", + procedureDate: sl.procedureDate + ? String(sl.procedureDate).split("T")[0] + : claimDate, + quad: sl.quad ?? "", + arch: sl.arch ?? "", + toothNumber: sl.toothNumber ?? "", + toothSurface: sl.toothSurface ?? "", + totalBilled: new Decimal(Number(sl.totalBilled ?? 0)), + totalAdjusted: new Decimal(Number(sl.totalAdjusted ?? 0)), + totalPaid: new Decimal(Number(sl.totalPaid ?? 0)), + })); + } setForm((prev) => ({ ...prev, claimId: claim.id, serviceDate: claimDate || prev.serviceDate, - serviceLines: mappedLines.length > 0 ? mappedLines : prev.serviceLines, + serviceLines: serviceLines.length > 0 ? serviceLines : prev.serviceLines, remarks: claim.remarks ?? "", missingTeethStatus: (claim.missingTeethStatus as MissingTeethStatus) ?? "No_missing", missingTeeth: (claim.missingTeeth as Record) ?? {}, @@ -2168,10 +2196,10 @@ export function ClaimForm({ {proceduresOnly ? (
- -