fix: D0140 claim sync, schedule column prefill, multi-appt, DentaQuest OTP session

- 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 <noreply@anthropic.com>
This commit is contained in:
ff
2026-05-28 15:39:36 -04:00
parent 0b3cc241bf
commit b20dc8e976
8 changed files with 181 additions and 281 deletions

View File

@@ -8,25 +8,7 @@ import {
const router = Router(); const router = Router();
/** // Specific routes must be registered before generic /:id routes to avoid shadowing.
* 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" });
}
});
router.get( router.get(
"/prefill-from-appointment/:appointmentId", "/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 * PUT /api/appointment-procedures/set-npi-provider/:appointmentId
* Set the npiProviderId on all procedures for an appointment (lightweight update). * 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 * DELETE /api/appointment-procedures/clear/:appointmentId
* Clear all procedures for appointment * 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; export default router;

View File

@@ -193,20 +193,13 @@ router.post(
const originalStartTime = appointmentData.startTime; const originalStartTime = appointmentData.startTime;
const MAX_END_TIME = "18:30"; 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); const patient = await storage.getPatient(appointmentData.patientId);
if (!patient) { if (!patient) {
return res.status(404).json({ message: "Patient not found" }); 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 // 2. Find the next available slot for this staff member (multiple same-patient same-day bookings allowed)
const existingPatientAppointment = await storage.getPatientAppointmentByDateAndStaff(
appointmentData.patientId,
appointmentData.date,
appointmentData.staffId
);
// 3. Attempt to find the next available slot
let [hour, minute] = originalStartTime.split(":").map(Number); let [hour, minute] = originalStartTime.split(":").map(Number);
const pad = (n: number) => n.toString().padStart(2, "0"); const pad = (n: number) => n.toString().padStart(2, "0");
@@ -216,45 +209,24 @@ router.post(
while (`${pad(hour)}:${pad(minute)}` <= MAX_END_TIME) { while (`${pad(hour)}:${pad(minute)}` <= MAX_END_TIME) {
const currentStartTime = `${pad(hour)}:${pad(minute)}`; 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( const staffConflict = await storage.getStaffAppointmentByDateTime(
appointmentData.staffId, appointmentData.staffId,
appointmentData.date, appointmentData.date,
currentStartTime, currentStartTime,
existingPatientAppointment?.id undefined
); );
if (!staffConflict) { if (!staffConflict) {
const endMinute = minute + APPT_DURATION_MINUTES; const endMinute = minute + APPT_DURATION_MINUTES;
let endHour = hour + Math.floor(endMinute / 60); const endHour = hour + Math.floor(endMinute / 60);
let realEndMinute = endMinute % 60; const realEndMinute = endMinute % 60;
const currentEndTime = `${pad(endHour)}:${pad(realEndMinute)}`; const currentEndTime = `${pad(endHour)}:${pad(realEndMinute)}`;
const payload = { const created = await storage.createAppointment({
...appointmentData, ...appointmentData,
startTime: currentStartTime, startTime: currentStartTime,
endTime: currentEndTime, 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({ return res.status(201).json({
...created, ...created,
originalRequestedTime: originalStartTime, originalRequestedTime: originalStartTime,
@@ -266,7 +238,6 @@ router.post(
}); });
} }
// Move to next STEP_MINUTES slot
minute += STEP_MINUTES; minute += STEP_MINUTES;
if (minute >= 60) { if (minute >= 60) {
hour += Math.floor(minute / 60); hour += Math.floor(minute / 60);
@@ -275,8 +246,7 @@ router.post(
} }
return res.status(409).json({ return res.status(409).json({
message: message: "No available slots remaining until 6:30 PM for this staff member. Please choose another day.",
"No available slots remaining until 6:30 PM for this Staff. Please choose another day.",
}); });
} catch (error) { } catch (error) {
console.error("Error in upsert appointment:", error); console.error("Error in upsert appointment:", error);

View File

@@ -4,7 +4,7 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { AppointmentForm } from "./appointment-form"; import { AppointmentForm, type NewAppointmentPrefill } from "./appointment-form";
import { import {
Appointment, Appointment,
InsertAppointment, InsertAppointment,
@@ -18,6 +18,7 @@ interface AddAppointmentModalProps {
onDelete?: (id: number) => void; onDelete?: (id: number) => void;
isLoading: boolean; isLoading: boolean;
appointment?: Appointment; appointment?: Appointment;
prefillData?: NewAppointmentPrefill | null;
} }
export function AddAppointmentModal({ export function AddAppointmentModal({
@@ -27,6 +28,7 @@ export function AddAppointmentModal({
onDelete, onDelete,
isLoading, isLoading,
appointment, appointment,
prefillData,
}: AddAppointmentModalProps) { }: AddAppointmentModalProps) {
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
@@ -39,6 +41,7 @@ export function AddAppointmentModal({
<div className="p-1"> <div className="p-1">
<AppointmentForm <AppointmentForm
appointment={appointment} appointment={appointment}
prefillData={prefillData}
onSubmit={(data) => { onSubmit={(data) => {
onSubmit(data); onSubmit(data);
onOpenChange(false); onOpenChange(false);

View File

@@ -37,8 +37,18 @@ import { DateInputField } from "@/components/ui/dateInputField";
import { formatLocalDate, parseLocalDate } from "@/utils/dateUtils"; import { formatLocalDate, parseLocalDate } from "@/utils/dateUtils";
import { toast } from "@/hooks/use-toast"; import { toast } from "@/hooks/use-toast";
export interface NewAppointmentPrefill {
staffId: number;
date: string;
startTime: string;
endTime: string;
patientId?: number;
type?: string;
}
interface AppointmentFormProps { interface AppointmentFormProps {
appointment?: Appointment; appointment?: Appointment;
prefillData?: NewAppointmentPrefill | null;
onSubmit: (data: InsertAppointment | UpdateAppointment) => void; onSubmit: (data: InsertAppointment | UpdateAppointment) => void;
onDelete?: (id: number) => void; onDelete?: (id: number) => void;
onOpenChange?: (open: boolean) => void; onOpenChange?: (open: boolean) => void;
@@ -47,6 +57,7 @@ interface AppointmentFormProps {
export function AppointmentForm({ export function AppointmentForm({
appointment, appointment,
prefillData,
onSubmit, onSubmit,
onDelete, onDelete,
onOpenChange, onOpenChange,
@@ -87,19 +98,6 @@ export function AppointmentForm({
color: colorMap[staff.name] || "bg-gray-400", 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 // Format the date and times for the form
const defaultValues: Partial<Appointment> = appointment const defaultValues: Partial<Appointment> = appointment
? { ? {
@@ -107,8 +105,8 @@ export function AppointmentForm({
patientId: appointment.patientId, patientId: appointment.patientId,
title: appointment.title, title: appointment.title,
date: parseLocalDate(appointment.date), date: parseLocalDate(appointment.date),
startTime: appointment.startTime || "09:00", // Default "09:00" startTime: appointment.startTime || "09:00",
endTime: appointment.endTime || "09:30", // Default "09:30" endTime: appointment.endTime || "09:30",
type: appointment.type?.startsWith("other:") ? "other" : appointment.type, type: appointment.type?.startsWith("other:") ? "other" : appointment.type,
notes: appointment.notes || "", notes: appointment.notes || "",
status: appointment.status || "scheduled", status: appointment.status || "scheduled",
@@ -117,23 +115,18 @@ export function AppointmentForm({
? appointment.staffId ? appointment.staffId
: undefined, : undefined,
} }
: parsedStoredData : prefillData
? { ? {
userId: user?.id, userId: user?.id,
patientId: Number(parsedStoredData.patientId), patientId: prefillData.patientId,
date: parsedStoredData.date date: prefillData.date ? parseLocalDate(prefillData.date) : new Date(),
? parseLocalDate(parsedStoredData.date) title: "",
: parseLocalDate(new Date()), startTime: prefillData.startTime,
title: parsedStoredData.title || "", endTime: prefillData.endTime,
startTime: parsedStoredData.startTime, type: prefillData.type || "checkup",
endTime: parsedStoredData.endTime, status: "scheduled",
type: parsedStoredData.type || "checkup", notes: "",
status: parsedStoredData.status || "scheduled", staffId: prefillData.staffId,
notes: parsedStoredData.notes || "",
staffId:
typeof parsedStoredData.staff === "number"
? parsedStoredData.staff
: (staffMembers?.[0]?.id ?? undefined),
} }
: { : {
userId: user?.id ?? 0, userId: user?.id ?? 0,
@@ -192,67 +185,24 @@ export function AppointmentForm({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectOpen]); }, [selectOpen]);
// Force form field values to update and clean up storage // Prefill form from prefillData prop (new appointment slot click)
useEffect(() => { useEffect(() => {
if (!parsedStoredData) return; if (!prefillData) return;
form.setValue("staffId", prefillData.staffId);
// set times/staff/date as before form.setValue("startTime", prefillData.startTime);
if (parsedStoredData.startTime) form.setValue("endTime", prefillData.endTime);
form.setValue("startTime", parsedStoredData.startTime); form.setValue("date", parseLocalDate(prefillData.date));
if (parsedStoredData.endTime) if (prefillData.type) form.setValue("type", prefillData.type);
form.setValue("endTime", parsedStoredData.endTime); if (prefillData.patientId) {
if (parsedStoredData.staff) form.setValue("patientId", prefillData.patientId);
form.setValue("staffId", parsedStoredData.staff);
if (parsedStoredData.date) {
form.setValue("date", parseLocalDate(parsedStoredData.date));
}
// ---- 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 () => { (async () => {
try { try {
const res = await apiRequest("GET", `/api/patients/${pid}`); const res = await apiRequest("GET", `/api/patients/${prefillData.patientId}`);
if (res.ok) { if (res.ok) setPrefillPatient(await res.json());
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 {} } 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 { }, [prefillData]);
// no patientId in storage — still remove to avoid stale state
sessionStorage.removeItem("newAppointmentData");
}
}, [form]);
// When editing an appointment, ensure we prefill the patient so SelectValue can render // When editing an appointment, ensure we prefill the patient so SelectValue can render
useEffect(() => { useEffect(() => {

View File

@@ -331,8 +331,35 @@ export function ClaimForm({
} catch {} } catch {}
} }
// Restore service lines // Prefer AppointmentProcedure records for service lines — they reflect any
const mappedLines = (claim.serviceLines ?? []).map((sl: 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 ?? "", procedureCode: sl.procedureCode ?? "",
procedureDate: sl.procedureDate procedureDate: sl.procedureDate
? String(sl.procedureDate).split("T")[0] ? String(sl.procedureDate).split("T")[0]
@@ -345,12 +372,13 @@ export function ClaimForm({
totalAdjusted: new Decimal(Number(sl.totalAdjusted ?? 0)), totalAdjusted: new Decimal(Number(sl.totalAdjusted ?? 0)),
totalPaid: new Decimal(Number(sl.totalPaid ?? 0)), totalPaid: new Decimal(Number(sl.totalPaid ?? 0)),
})); }));
}
setForm((prev) => ({ setForm((prev) => ({
...prev, ...prev,
claimId: claim.id, claimId: claim.id,
serviceDate: claimDate || prev.serviceDate, serviceDate: claimDate || prev.serviceDate,
serviceLines: mappedLines.length > 0 ? mappedLines : prev.serviceLines, serviceLines: serviceLines.length > 0 ? serviceLines : prev.serviceLines,
remarks: claim.remarks ?? "", remarks: claim.remarks ?? "",
missingTeethStatus: (claim.missingTeethStatus as MissingTeethStatus) ?? "No_missing", missingTeethStatus: (claim.missingTeethStatus as MissingTeethStatus) ?? "No_missing",
missingTeeth: (claim.missingTeeth as Record<string, "X" | "O">) ?? {}, missingTeeth: (claim.missingTeeth as Record<string, "X" | "O">) ?? {},
@@ -2168,10 +2196,10 @@ export function ClaimForm({
</h3> </h3>
{proceduresOnly ? ( {proceduresOnly ? (
<div className="flex justify-center gap-3"> <div className="flex justify-center gap-3">
<Button className="w-40" variant="default" onClick={handleProceduresSave}> <Button className="w-40" variant="default" onClick={() => runWithPriceCheck(handleProceduresSave)}>
Save Procedures Save Procedures
</Button> </Button>
<Button className="w-40" variant="secondary" onClick={handleProceduresUpdate}> <Button className="w-40" variant="secondary" onClick={() => runWithPriceCheck(handleProceduresUpdate)}>
Update &amp; Resubmit Update &amp; Resubmit
</Button> </Button>
<Button className="w-28" variant="destructive" onClick={handleProceduresVoid}> <Button className="w-28" variant="destructive" onClick={handleProceduresVoid}>

View File

@@ -8,6 +8,7 @@ import {
formatDateToHumanReadable, formatDateToHumanReadable,
} from "@/utils/dateUtils"; } from "@/utils/dateUtils";
import { AddAppointmentModal } from "@/components/appointments/add-appointment-modal"; import { AddAppointmentModal } from "@/components/appointments/add-appointment-modal";
import type { NewAppointmentPrefill } from "@/components/appointments/appointment-form";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Calendar as CalendarIcon, Calendar as CalendarIcon,
@@ -177,6 +178,7 @@ export default function AppointmentsPage() {
timeSlot: TimeSlot; timeSlot: TimeSlot;
staffId: number; staffId: number;
} | null>(null); } | null>(null);
const [newApptPrefill, setNewApptPrefill] = useState<NewAppointmentPrefill | null>(null);
const toggleReminderColumn = (staffId: number) => { const toggleReminderColumn = (staffId: number) => {
setSelectedReminderColumns((prev) => { setSelectedReminderColumns((prev) => {
@@ -366,53 +368,21 @@ export default function AppointmentsPage() {
// Check for newPatient parameter in URL // Check for newPatient parameter in URL
useEffect(() => { useEffect(() => {
// Parse URL search params to check for newPatient
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const newPatientId = params.get("newPatient"); const newPatientId = params.get("newPatient");
if (newPatientId) { if (newPatientId) {
const patientId = parseInt(newPatientId); const patientId = parseInt(newPatientId);
// Choose first available staff safely (fallback to 1 if none) const firstStaff = staffMembers.length > 0 ? staffMembers[0] : undefined;
const firstStaff =
staffMembers && staffMembers.length > 0 ? staffMembers[0] : undefined;
const staffId = firstStaff ? Number(firstStaff.id) : 1; const staffId = firstStaff ? Number(firstStaff.id) : 1;
// Find first time slot today (9:00 AM is a common starting time) const defaultTimeSlot = timeSlots.find((slot) => slot.time === "09:00") || timeSlots[0];
const defaultTimeSlot =
timeSlots.find((slot) => slot.time === "09:00") || timeSlots[0];
if (!defaultTimeSlot) { if (!defaultTimeSlot) {
toast({ toast({ title: "Unable to schedule", description: "No available time slots.", variant: "destructive" });
title: "Unable to schedule",
description:
"No available time slots to schedule the new patient right now.",
variant: "destructive",
});
return; return;
} }
// Merge any existing "newAppointmentData" with the patient info BEFORE opening modal handleCreateAppointmentAtSlot(defaultTimeSlot, staffId, patientId);
try {
const existingRaw = sessionStorage.getItem("newAppointmentData");
const existing = existingRaw ? JSON.parse(existingRaw) : {};
const newAppointmentData = {
...existing,
patientId: patientId,
};
sessionStorage.setItem(
"newAppointmentData",
JSON.stringify(newAppointmentData)
);
} catch (err) {
// If sessionStorage parsing fails, overwrite with a fresh object
sessionStorage.setItem(
"newAppointmentData",
JSON.stringify({ patientId: patientId })
);
}
// Open/create the appointment modal (will read sessionStorage in the modal)
handleCreateAppointmentAtSlot(defaultTimeSlot, Number(staffId));
// Remove the query param from the URL so this doesn't re-run on navigation/refresh
params.delete("newPatient"); params.delete("newPatient");
const newSearch = params.toString(); const newSearch = params.toString();
const newUrl = `${window.location.pathname}${newSearch ? `?${newSearch}` : ""}${window.location.hash || ""}`; const newUrl = `${window.location.pathname}${newSearch ? `?${newSearch}` : ""}${window.location.hash || ""}`;
@@ -671,9 +641,9 @@ export default function AppointmentsPage() {
// Handle creating a new appointment at a specific time slot and for a specific staff member // Handle creating a new appointment at a specific time slot and for a specific staff member
const handleCreateAppointmentAtSlot = ( const handleCreateAppointmentAtSlot = (
timeSlot: TimeSlot, timeSlot: TimeSlot,
staffId: number staffId: number,
patientId?: number
) => { ) => {
// Calculate end time (30 minutes after start time)
const startHour = parseInt(timeSlot.time.split(":")[0] as string); const startHour = parseInt(timeSlot.time.split(":")[0] as string);
const startMinute = parseInt(timeSlot.time.split(":")[1] as string); const startMinute = parseInt(timeSlot.time.split(":")[1] as string);
const startDate = parseLocalDate(selectedDate); const startDate = parseLocalDate(selectedDate);
@@ -682,59 +652,17 @@ export default function AppointmentsPage() {
const endDate = addMinutes(startDate, 30); const endDate = addMinutes(startDate, 30);
const endTime = `${endDate.getHours().toString().padStart(2, "0")}:${endDate.getMinutes().toString().padStart(2, "0")}`; const endTime = `${endDate.getHours().toString().padStart(2, "0")}:${endDate.getMinutes().toString().padStart(2, "0")}`;
// Find staff member
const staff = staffMembers.find((s) => Number(s.id) === Number(staffId)); const staff = staffMembers.find((s) => Number(s.id) === Number(staffId));
// Try to read any existing prefill data (may include patientId from the URL handler) setNewApptPrefill({
let existingStored: any = null; staffId: Number(staffId),
try {
const raw = sessionStorage.getItem("newAppointmentData");
existingStored = raw ? JSON.parse(raw) : null;
} catch (e) {
// ignore parse errors and treat as no existing stored data
existingStored = null;
}
// Build the prefill appointment object and merge existing stored data
const newAppointment = {
// base defaults
date: formatLocalDate(selectedDate), date: formatLocalDate(selectedDate),
startTime: timeSlot.time, // This is in "HH:MM" format startTime: timeSlot.time,
endTime: endTime, endTime,
type: staff?.role === "doctor" ? "checkup" : "cleaning", type: staff?.role === "doctor" ? "checkup" : "cleaning",
status: "scheduled", patientId,
title: `Appointment with ${staff?.name}`, });
notes: `Appointment with ${staff?.name}`,
staff: Number(staffId), // consistent field name that matches update mutation
// if existingStored has patientId (or other fields) merge them below
...(existingStored || {}),
};
// Ensure explicit values from this function override stale values from storage
// (for example, prefer current slot and staff)
const mergedAppointment = {
...newAppointment,
date: newAppointment.date,
startTime: newAppointment.startTime,
endTime: newAppointment.endTime,
staff: Number(staffId),
};
// Persist merged prefill so the modal/form can read it
try {
sessionStorage.setItem(
"newAppointmentData",
JSON.stringify(mergedAppointment)
);
} catch (e) {
// ignore sessionStorage write failures
console.error("Failed to write newAppointmentData to sessionStorage", e);
}
// For new appointments, set editingAppointment to undefined
setEditingAppointment(undefined); setEditingAppointment(undefined);
// Open modal
setIsAddModalOpen(true); setIsAddModalOpen(true);
}; };
@@ -1898,13 +1826,17 @@ export default function AppointmentsPage() {
{/* Add/Edit Appointment Modal */} {/* Add/Edit Appointment Modal */}
<AddAppointmentModal <AddAppointmentModal
open={isAddModalOpen} open={isAddModalOpen}
onOpenChange={setIsAddModalOpen} onOpenChange={(open) => {
setIsAddModalOpen(open);
if (!open) setNewApptPrefill(null);
}}
onSubmit={handleAppointmentSubmit} onSubmit={handleAppointmentSubmit}
isLoading={ isLoading={
createAppointmentMutation.isPending || createAppointmentMutation.isPending ||
updateAppointmentMutation.isPending updateAppointmentMutation.isPending
} }
appointment={editingAppointment} appointment={editingAppointment}
prefillData={editingAppointment ? null : newApptPrefill}
onDelete={handleDeleteAppointment} onDelete={handleDeleteAppointment}
/> />

View File

@@ -296,13 +296,20 @@ async def start_tuftssco_claim_run(sid: str, data: dict, url: str):
s["result"] = result s["result"] = result
s["message"] = "completed" s["message"] = "completed"
# Close browser window (session preserved in profile) # Navigate away and minimize — keeps session cookie in memory so next
# eligibility check or claim run skips OTP entirely.
try: try:
from dentaquest_browser_manager import get_browser_manager as _gbm bot.driver.get("about:blank")
_gbm().quit_driver() try:
print("[TuftsSCO Claim] Browser closed - session preserved in profile") bot.driver.minimize_window()
except Exception:
try:
bot.driver.set_window_position(-10000, -10000)
except Exception:
pass
print("[TuftsSCO Claim] Browser minimized - session preserved for next run")
except Exception as close_err: except Exception as close_err:
print(f"[TuftsSCO Claim] Could not close browser (non-fatal): {close_err}") print(f"[TuftsSCO Claim] Could not minimize browser (non-fatal): {close_err}")
asyncio.create_task(_remove_session_later(sid, 60)) asyncio.create_task(_remove_session_later(sid, 60))
return result return result

View File

@@ -694,13 +694,21 @@ class AutomationDentaQuestEligibilityCheck:
f.write(pdf_data) f.write(pdf_data)
print(f"[DentaQuest step2] PDF saved: {pdf_path}") print(f"[DentaQuest step2] PDF saved: {pdf_path}")
# Close browser after PDF (session preserved in profile) # Navigate away and minimize — keeps session cookie in memory so next
# run detects ALREADY_LOGGED_IN and skips OTP entirely.
# (OTP is only needed once per app startup, not on every check.)
try: try:
from dentaquest_browser_manager import get_browser_manager self.driver.get("about:blank")
get_browser_manager().quit_driver() try:
print("[DentaQuest step2] Browser closed") self.driver.minimize_window()
except Exception:
try:
self.driver.set_window_position(-10000, -10000)
except Exception:
pass
print("[DentaQuest step2] Browser minimized - session preserved for next run")
except Exception as e: except Exception as e:
print(f"[DentaQuest step2] Error closing browser: {e}") print(f"[DentaQuest step2] Error minimizing browser (non-fatal): {e}")
output = { output = {
"status": "success", "status": "success",