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,
|
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", {
|
const job = await seleniumQueue.add("claim-submit", {
|
||||||
jobType: "claim-submit",
|
jobType: "claim-submit",
|
||||||
userId: req.user.id,
|
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 type { OfficeHoursData } from "@/components/settings/office-hours-card";
|
||||||
import { MessageThread } from "@/components/patient-connection/message-thread";
|
import { MessageThread } from "@/components/patient-connection/message-thread";
|
||||||
import { getAppointmentTypeLabel } from "@/utils/appointmentTypeUtils";
|
import { getAppointmentTypeLabel } from "@/utils/appointmentTypeUtils";
|
||||||
|
import { getDescriptionForCode } from "@/utils/procedureCombosMapping";
|
||||||
|
|
||||||
// Define types for scheduling
|
// Define types for scheduling
|
||||||
interface TimeSlot {
|
interface TimeSlot {
|
||||||
@@ -110,6 +111,27 @@ interface ScheduledAppointment {
|
|||||||
procedureCodes?: string[];
|
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 {
|
function appointmentCardColor(apt: ScheduledAppointment): string {
|
||||||
if (apt.hasClaimWithNumber) return "bg-gray-500 text-white";
|
if (apt.hasClaimWithNumber) return "bg-gray-500 text-white";
|
||||||
if (apt.hasProcedures) return "bg-blue-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 apt = queue[index]!;
|
||||||
const aptDate = typeof apt.date === "string" ? apt.date : formatLocalDate(apt.date as Date);
|
const aptDate = typeof apt.date === "string" ? apt.date : formatLocalDate(apt.date as Date);
|
||||||
if (!apt.notes?.trim()) {
|
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);
|
setIsAiClaimProcessing(false);
|
||||||
setAiClaimCurrentData({
|
setAiClaimCurrentData({
|
||||||
matchedCodes: [],
|
matchedCodes: [],
|
||||||
|
|||||||
Reference in New Issue
Block a user