feat: fall back to saved procedures for no-notes appointments in AI claim queue, and include appointment attachments in MH selenium submission
- AI claim queue now uses previously saved AppointmentProcedure data (with a derived insurance site key) when an appointment has no notes but has saved procedures, instead of skipping with "No notes on this appointment" - selenium-claim route now reads saved AppointmentFile attachments from disk when they weren't re-uploaded in the browser, so images/PDFs already attached via "Select Procedures" reach the MassHealth submission
This commit is contained in:
@@ -346,6 +346,24 @@ router.post(
|
||||
mimetype: f.mimetype,
|
||||
}));
|
||||
|
||||
// Include attachments already saved on the appointment (e.g. via "Select Procedures")
|
||||
// that weren't re-uploaded in this request — read straight from disk by filePath.
|
||||
if (claimData.appointmentId) {
|
||||
const seenNames = new Set(filesForQueue.map((f) => f.originalname));
|
||||
const apptFiles = await storage.getAppointmentFiles(Number(claimData.appointmentId));
|
||||
for (const f of apptFiles) {
|
||||
if (!f.filePath || seenNames.has(f.filename)) continue;
|
||||
const absPath = path.join(process.cwd(), f.filePath);
|
||||
if (!fs.existsSync(absPath)) continue;
|
||||
filesForQueue.push({
|
||||
originalname: f.filename,
|
||||
bufferBase64: fs.readFileSync(absPath).toString("base64"),
|
||||
mimetype: f.mimeType ?? "application/octet-stream",
|
||||
});
|
||||
seenNames.add(f.filename);
|
||||
}
|
||||
}
|
||||
|
||||
const job = await seleniumQueue.add("claim-submit", {
|
||||
jobType: "claim-submit",
|
||||
userId: req.user.id,
|
||||
|
||||
@@ -76,6 +76,7 @@ import { PatientStatusBadge } from "@/components/appointments/patient-status-bad
|
||||
import type { OfficeHoursData } from "@/components/settings/office-hours-card";
|
||||
import { MessageThread } from "@/components/patient-connection/message-thread";
|
||||
import { getAppointmentTypeLabel } from "@/utils/appointmentTypeUtils";
|
||||
import { getDescriptionForCode } from "@/utils/procedureCombosMapping";
|
||||
|
||||
// Define types for scheduling
|
||||
interface TimeSlot {
|
||||
@@ -110,6 +111,27 @@ interface ScheduledAppointment {
|
||||
procedureCodes?: string[];
|
||||
}
|
||||
|
||||
function deriveInsuranceSiteKey(provider: string | null | undefined): string {
|
||||
const p = (provider || "").toLowerCase().trim();
|
||||
if (!p) return "";
|
||||
if (p.includes("masshealth") || p === "mh" || p === "mass health") return "MH";
|
||||
if (p.includes("commonwealth care alliance") || p === "cca") return "CCA";
|
||||
if (p.includes("ddma") || p.includes("delta dental ma")) return "DDMA";
|
||||
if (p.includes("delta ins") || p === "deltains") return "DeltaIns";
|
||||
if (p.includes("tufts") || p.includes("dentaquest") || p === "tuftssco") return "TuftsSCO";
|
||||
if ((p.includes("united") && p.includes("sco")) || p === "unitedsco") return "UnitedSCO";
|
||||
if (p.includes("cmsp")) return "CMSP";
|
||||
if (p.includes("bcbs") || p.includes("blue cross")) return "BCBS";
|
||||
if (p.includes("united aapr") || p === "unitedaapr") return "UnitedAAPR";
|
||||
if (p.includes("aetna")) return "Aetna";
|
||||
if (p.includes("altus")) return "Altus";
|
||||
if (p.includes("metlife")) return "MetlifeDental";
|
||||
if (p.includes("cigna")) return "Cigna";
|
||||
if (p.includes("delta wa") || p === "deltawa") return "DeltaWA";
|
||||
if (p.includes("delta il") || p === "deltail") return "DeltaIL";
|
||||
return "";
|
||||
}
|
||||
|
||||
function appointmentCardColor(apt: ScheduledAppointment): string {
|
||||
if (apt.hasClaimWithNumber) return "bg-gray-500 text-white";
|
||||
if (apt.hasProcedures) return "bg-blue-500 text-white";
|
||||
@@ -1515,6 +1537,42 @@ export default function AppointmentsPage() {
|
||||
const apt = queue[index]!;
|
||||
const aptDate = typeof apt.date === "string" ? apt.date : formatLocalDate(apt.date as Date);
|
||||
if (!apt.notes?.trim()) {
|
||||
if (apt.hasProcedures) {
|
||||
setIsAiClaimProcessing(true);
|
||||
setAiClaimCurrentData(null);
|
||||
try {
|
||||
const res = await apiRequest("GET", `/api/appointment-procedures/prefill-from-appointment/${apt.id}`);
|
||||
const data = await res.json();
|
||||
const matchedCodes = (data.procedures ?? []).map((p: any) => ({
|
||||
code: p.procedureCode,
|
||||
description: getDescriptionForCode(p.procedureCode) ?? "",
|
||||
toothNumber: p.toothNumber ?? undefined,
|
||||
toothSurface: p.toothSurface ?? undefined,
|
||||
}));
|
||||
setAiClaimCurrentData({
|
||||
matchedCodes,
|
||||
siteKey: deriveInsuranceSiteKey(apt.patientInsuranceProvider),
|
||||
serviceDate: aptDate,
|
||||
appointmentId: Number(apt.id),
|
||||
patientName: apt.patientName,
|
||||
notes: "",
|
||||
reply: "No notes on this appointment — using previously saved procedures instead.",
|
||||
});
|
||||
} catch {
|
||||
setAiClaimCurrentData({
|
||||
matchedCodes: [],
|
||||
siteKey: "",
|
||||
serviceDate: aptDate,
|
||||
appointmentId: Number(apt.id),
|
||||
patientName: apt.patientName,
|
||||
notes: "",
|
||||
reply: "No notes on this appointment, and failed to load saved procedures.",
|
||||
});
|
||||
} finally {
|
||||
setIsAiClaimProcessing(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
setIsAiClaimProcessing(false);
|
||||
setAiClaimCurrentData({
|
||||
matchedCodes: [],
|
||||
|
||||
Reference in New Issue
Block a user