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();
/**
* 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;

View File

@@ -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);