fix: fix remote browser socket connection and related updates
This commit is contained in:
@@ -84,7 +84,7 @@ router.put("/set-npi-provider/:appointmentId", async (req: Request, res: Respons
|
||||
*/
|
||||
router.post("/save-for-appointment", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { appointmentId, patientId, npiProviderId, procedures } = req.body;
|
||||
const { appointmentId, patientId, npiProviderId, procedures, attachments } = req.body;
|
||||
|
||||
if (!appointmentId || isNaN(Number(appointmentId))) {
|
||||
return res.status(400).json({ message: "Invalid appointmentId" });
|
||||
@@ -100,6 +100,14 @@ router.post("/save-for-appointment", async (req: Request, res: Response) => {
|
||||
(p) => String(p.procedureCode ?? "").trim() !== ""
|
||||
);
|
||||
|
||||
const validAttachments = Array.isArray(attachments)
|
||||
? (attachments as any[]).filter((a) => a?.filename).map((a) => ({
|
||||
filename: String(a.filename),
|
||||
mimeType: a.mimeType ?? null,
|
||||
filePath: a.filePath ?? null,
|
||||
}))
|
||||
: undefined;
|
||||
|
||||
const count = await storage.saveForAppointment({
|
||||
appointmentId: Number(appointmentId),
|
||||
patientId: Number(patientId),
|
||||
@@ -110,6 +118,7 @@ router.post("/save-for-appointment", async (req: Request, res: Response) => {
|
||||
toothNumber: p.toothNumber || null,
|
||||
toothSurface: p.toothSurface || null,
|
||||
})),
|
||||
attachments: validAttachments,
|
||||
});
|
||||
|
||||
return res.json({ success: true, count });
|
||||
|
||||
@@ -425,18 +425,25 @@ router.post(
|
||||
// Fetch active claim for this appointment (includes service lines from draft saves)
|
||||
const activeClaim = await storage.getActiveClaimByAppointmentId(Number(apt.id));
|
||||
|
||||
// "Already claimed" = has a real claim number OR status is REVIEW/APPROVED
|
||||
// A PENDING claim with no claimNumber is just a draft save — not yet submitted
|
||||
const alreadyClaimed =
|
||||
// Skip if claim was voided via the "Void" button in Select Procedures.
|
||||
if (activeClaim?.status === "VOID") {
|
||||
resultItem.skipped = true;
|
||||
resultItem.error = "Voided";
|
||||
results.push(resultItem);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip appointments whose claim was already submitted (has claimNumber or REVIEW/APPROVED).
|
||||
// The "Update & Resubmit" button resets the claim to PENDING so it is picked up again.
|
||||
const alreadySubmitted =
|
||||
activeClaim &&
|
||||
((activeClaim.claimNumber != null &&
|
||||
String(activeClaim.claimNumber).trim() !== "") ||
|
||||
((activeClaim.claimNumber != null && String(activeClaim.claimNumber).trim() !== "") ||
|
||||
activeClaim.status === "REVIEW" ||
|
||||
activeClaim.status === "APPROVED");
|
||||
|
||||
if (alreadyClaimed) {
|
||||
if (alreadySubmitted) {
|
||||
resultItem.skipped = true;
|
||||
resultItem.error = "Already claimed";
|
||||
resultItem.error = "Already submitted";
|
||||
results.push(resultItem);
|
||||
continue;
|
||||
}
|
||||
@@ -544,7 +551,6 @@ router.post(
|
||||
let claimId: number;
|
||||
if (activeClaim?.id) {
|
||||
claimId = activeClaim.id;
|
||||
// Update claim's npiProviderId if the user chose a different provider via Select Procedures
|
||||
if (procNpiProviderId && activeClaim.npiProviderId !== procNpiProviderId) {
|
||||
await storage.updateClaim(claimId, { npiProviderId: procNpiProviderId });
|
||||
}
|
||||
@@ -634,12 +640,31 @@ router.post(
|
||||
};
|
||||
}
|
||||
|
||||
// Collect attachments: appointment-level files + claim-level files
|
||||
const apptFiles = await storage.getAppointmentFiles(Number(apt.id));
|
||||
const claimFiles = (activeClaim as any)?.claimFiles ?? [];
|
||||
const allFileMeta = [
|
||||
...apptFiles,
|
||||
...claimFiles,
|
||||
] as Array<{ filename: string; mimeType?: string | null; filePath?: string | null }>;
|
||||
|
||||
const filesForQueue = allFileMeta.flatMap((f) => {
|
||||
if (!f.filePath) return [];
|
||||
const absPath = path.join(process.cwd(), f.filePath);
|
||||
if (!fs.existsSync(absPath)) {
|
||||
console.warn(`[batch-column] attachment not found on disk: ${absPath}`);
|
||||
return [];
|
||||
}
|
||||
const bufferBase64 = fs.readFileSync(absPath).toString("base64");
|
||||
return [{ originalname: f.filename, bufferBase64, mimetype: f.mimeType ?? "application/octet-stream" }];
|
||||
});
|
||||
|
||||
// Enqueue selenium claim-submit job
|
||||
const job = await seleniumQueue.add("claim-submit", {
|
||||
jobType: "claim-submit",
|
||||
userId: req.user.id,
|
||||
enrichedPayload,
|
||||
files: [],
|
||||
files: filesForQueue,
|
||||
claimId,
|
||||
});
|
||||
|
||||
@@ -991,4 +1016,72 @@ router.delete("/:id", async (req: Request, res: Response): Promise<any> => {
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/claims/void-for-appointment
|
||||
// Marks the claim for an appointment as VOID so batch-column skips it permanently.
|
||||
// If no claim exists yet, creates a minimal placeholder VOID claim.
|
||||
router.post("/void-for-appointment", async (req: Request, res: Response): Promise<any> => {
|
||||
try {
|
||||
if (!req.user?.id) return res.status(401).json({ error: "Unauthorized" });
|
||||
const { appointmentId } = req.body;
|
||||
if (!appointmentId || isNaN(Number(appointmentId))) {
|
||||
return res.status(400).json({ error: "Invalid appointmentId" });
|
||||
}
|
||||
|
||||
const existing = await storage.getActiveClaimByAppointmentId(Number(appointmentId));
|
||||
if (existing) {
|
||||
await storage.updateClaim(Number(existing.id), { status: "VOID" } as any);
|
||||
return res.json({ voided: true, claimId: existing.id });
|
||||
}
|
||||
|
||||
// No claim yet — look up appointment + patient to create a minimal VOID placeholder
|
||||
const apt = await storage.getAppointment(Number(appointmentId));
|
||||
if (!apt) return res.status(404).json({ error: "Appointment not found" });
|
||||
const patient = apt.patientId ? await storage.getPatient(apt.patientId) : null;
|
||||
if (!patient) return res.status(404).json({ error: "Patient not found" });
|
||||
|
||||
const newClaim = await storage.createClaim({
|
||||
patientId: Number(patient.id),
|
||||
appointmentId: Number(appointmentId),
|
||||
userId: req.user.id,
|
||||
staffId: Number(apt.staffId),
|
||||
patientName: `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim(),
|
||||
memberId: String(patient.insuranceId ?? ""),
|
||||
dateOfBirth: patient.dateOfBirth ? new Date(patient.dateOfBirth) : new Date(),
|
||||
serviceDate: apt.date instanceof Date ? apt.date : new Date(apt.date as any),
|
||||
insuranceProvider: "MassHealth",
|
||||
remarks: "",
|
||||
missingTeethStatus: "No_missing",
|
||||
missingTeeth: {},
|
||||
status: "VOID",
|
||||
} as any);
|
||||
return res.json({ voided: true, claimId: newClaim.id });
|
||||
} catch (err: any) {
|
||||
console.error("void-for-appointment error", err);
|
||||
return res.status(500).json({ error: err.message ?? "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/claims/reset-for-resubmit
|
||||
// Resets the active claim for an appointment back to PENDING with no claimNumber,
|
||||
// so the batch-column will pick it up again on the next run.
|
||||
router.post("/reset-for-resubmit", async (req: Request, res: Response): Promise<any> => {
|
||||
try {
|
||||
const { appointmentId } = req.body;
|
||||
if (!appointmentId || isNaN(Number(appointmentId))) {
|
||||
return res.status(400).json({ error: "Invalid appointmentId" });
|
||||
}
|
||||
|
||||
const claim = await storage.getActiveClaimByAppointmentId(Number(appointmentId));
|
||||
if (!claim) {
|
||||
return res.json({ reset: false, message: "No existing claim found — will be created fresh on next submit" });
|
||||
}
|
||||
|
||||
await storage.updateClaim(Number(claim.id), { status: "PENDING", claimNumber: null } as any);
|
||||
return res.json({ reset: true, claimId: claim.id });
|
||||
} catch (err: any) {
|
||||
console.error("reset-for-resubmit error", err);
|
||||
return res.status(500).json({ error: err.message ?? "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -7,6 +7,12 @@ import {
|
||||
} from "@repo/db/types";
|
||||
import { prisma as db } from "@repo/db/client";
|
||||
|
||||
export interface AppointmentFileMeta {
|
||||
filename: string;
|
||||
mimeType?: string | null;
|
||||
filePath?: string | null;
|
||||
}
|
||||
|
||||
export interface IAppointmentProceduresStorage {
|
||||
getByAppointmentId(appointmentId: number): Promise<AppointmentProcedure[]>;
|
||||
getPrefillDataByAppointmentId(appointmentId: number): Promise<{
|
||||
@@ -14,6 +20,7 @@ export interface IAppointmentProceduresStorage {
|
||||
patient: Patient;
|
||||
procedures: AppointmentProcedure[];
|
||||
npiProviderId: number | null;
|
||||
appointmentFiles: AppointmentFileMeta[];
|
||||
} | null>;
|
||||
saveForAppointment(params: {
|
||||
appointmentId: number;
|
||||
@@ -25,6 +32,7 @@ export interface IAppointmentProceduresStorage {
|
||||
toothNumber?: string | null;
|
||||
toothSurface?: string | null;
|
||||
}>;
|
||||
attachments?: AppointmentFileMeta[];
|
||||
}): Promise<number>;
|
||||
|
||||
createProcedure(
|
||||
@@ -37,6 +45,7 @@ export interface IAppointmentProceduresStorage {
|
||||
): Promise<AppointmentProcedure>;
|
||||
deleteProcedure(id: number): Promise<void>;
|
||||
clearByAppointmentId(appointmentId: number): Promise<void>;
|
||||
getAppointmentFiles(appointmentId: number): Promise<AppointmentFileMeta[]>;
|
||||
getAppointmentIdsWithProcedures(ids: number[]): Promise<Set<number>>;
|
||||
}
|
||||
|
||||
@@ -58,6 +67,9 @@ export const appointmentProceduresStorage: IAppointmentProceduresStorage = {
|
||||
procedures: {
|
||||
orderBy: { createdAt: "asc" },
|
||||
},
|
||||
files: {
|
||||
orderBy: { id: "asc" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -72,11 +84,28 @@ export const appointmentProceduresStorage: IAppointmentProceduresStorage = {
|
||||
patient: appointment.patient,
|
||||
procedures: appointment.procedures,
|
||||
npiProviderId,
|
||||
appointmentFiles: (appointment.files as any[]).map((f) => ({
|
||||
id: f.id,
|
||||
filename: f.filename,
|
||||
mimeType: f.mimeType,
|
||||
filePath: f.filePath,
|
||||
})),
|
||||
};
|
||||
},
|
||||
|
||||
async saveForAppointment({ appointmentId, patientId, npiProviderId, procedures }) {
|
||||
async saveForAppointment({ appointmentId, patientId, npiProviderId, procedures, attachments }) {
|
||||
await db.appointmentProcedure.deleteMany({ where: { appointmentId } });
|
||||
if (attachments?.length) {
|
||||
await db.appointmentFile.deleteMany({ where: { appointmentId } });
|
||||
await db.appointmentFile.createMany({
|
||||
data: attachments.map((a) => ({
|
||||
appointmentId,
|
||||
filename: a.filename,
|
||||
mimeType: a.mimeType ?? null,
|
||||
filePath: a.filePath ?? null,
|
||||
})),
|
||||
});
|
||||
}
|
||||
if (!procedures.length) return 0;
|
||||
const result = await db.appointmentProcedure.createMany({
|
||||
data: procedures.map((p) => ({
|
||||
@@ -139,6 +168,19 @@ export const appointmentProceduresStorage: IAppointmentProceduresStorage = {
|
||||
select: { appointmentId: true },
|
||||
distinct: ["appointmentId"],
|
||||
});
|
||||
return new Set(rows.map((r) => r.appointmentId));
|
||||
return new Set(rows.map((r: any) => r.appointmentId));
|
||||
},
|
||||
|
||||
async getAppointmentFiles(appointmentId: number): Promise<AppointmentFileMeta[]> {
|
||||
const rows = await db.appointmentFile.findMany({
|
||||
where: { appointmentId },
|
||||
orderBy: { id: "asc" },
|
||||
});
|
||||
return rows.map((f: any) => ({
|
||||
id: f.id,
|
||||
filename: f.filename,
|
||||
mimeType: f.mimeType,
|
||||
filePath: f.filePath,
|
||||
}));
|
||||
},
|
||||
};
|
||||
|
||||
@@ -60,7 +60,7 @@ export const claimsStorage: IStorage = {
|
||||
return db.claim.findFirst({
|
||||
where: {
|
||||
appointmentId,
|
||||
status: { notIn: ["CANCELLED", "VOID"] },
|
||||
status: { notIn: ["CANCELLED"] },
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
include: { serviceLines: true, claimFiles: true, staff: true },
|
||||
|
||||
Reference in New Issue
Block a user