From 99cdfa702a1dfe3b08f56fd935e1b66bf3ab229f Mon Sep 17 00:00:00 2001 From: Gitead Date: Wed, 8 Jul 2026 00:20:37 -0400 Subject: [PATCH] feat: save insurance PDFs (eligibility/claims/preauth) to Cloud Storage instead of Postgres blobs New saves go into the patient's Cloud Storage folder under category subfolders (Eligibility/Claims/PreAuth/Claim Status/Attachments) via a new storage.savePdfToCloudStorage helper, instead of the legacy PdfGroup/PdfFile Postgres blob tables. Old records and their read paths are left untouched for backward compatibility; PDF list/content/delete endpoints and viewers now transparently handle both legacy numeric ids and new "cloud:" ids. Co-Authored-By: Claude Sonnet 5 --- apps/Backend/src/queue/jobRunner.ts | 2 + .../processors/bcbsMaEligibilityProcessor.ts | 25 ++--- .../src/queue/processors/ccaClaimProcessor.ts | 23 +++-- .../processors/ccaEligibilityProcessor.ts | 27 +++--- .../queue/processors/ccaPreAuthProcessor.ts | 23 +++-- .../queue/processors/claimStatusProcessor.ts | 30 +++--- .../queue/processors/claimSubmitProcessor.ts | 27 ++++-- ...mspEligibilityHistoryRemainingProcessor.ts | 32 +++---- .../queue/processors/ddmaClaimProcessor.ts | 17 +++- .../processors/ddmaEligibilityProcessor.ts | 27 +++--- .../deltaInsEligibilityProcessor.ts | 27 +++--- .../dentaQuestEligibilityProcessor.ts | 27 +++--- .../processors/eligibilityHistoryProcessor.ts | 34 +++---- .../queue/processors/eligibilityProcessor.ts | 24 ++--- .../processors/tuftsSCOClaimProcessor.ts | 17 +++- .../processors/tuftsSCOPreAuthProcessor.ts | 19 ++-- .../processors/unitedDHClaimProcessor.ts | 19 ++-- .../processors/unitedDHPreAuthProcessor.ts | 19 ++-- .../unitedSCOEligibilityProcessor.ts | 27 +++--- .../src/queue/workers/seleniumWorker.ts | 3 + apps/Backend/src/routes/claims.ts | 63 ++++++++----- apps/Backend/src/routes/documents.ts | 80 +++++++++++++++- apps/Backend/src/routes/insuranceStatus.ts | 30 ++---- .../src/storage/cloudStorage-storage.ts | 94 +++++++++++++++++++ .../documents/file-preview-modal.tsx | 6 +- .../insurance-status/bcbs-ma-button-modal.tsx | 4 +- .../insurance-status/cca-button-modal.tsx | 4 +- .../insurance-status/ddma-buton-modal.tsx | 6 +- .../deltains-button-modal.tsx | 4 +- .../dual-pdf-preview-modal.tsx | 4 +- .../insurance-status/pdf-preview-modal.tsx | 2 +- .../tufts-sco-button-modal.tsx | 4 +- .../united-sco-button-modal.tsx | 4 +- apps/Frontend/src/pages/claims-page.tsx | 2 +- apps/Frontend/src/pages/documents-page.tsx | 22 +++-- .../src/pages/insurance-status-page.tsx | 14 +-- 36 files changed, 496 insertions(+), 296 deletions(-) diff --git a/apps/Backend/src/queue/jobRunner.ts b/apps/Backend/src/queue/jobRunner.ts index 98016a69..13688a4e 100644 --- a/apps/Backend/src/queue/jobRunner.ts +++ b/apps/Backend/src/queue/jobRunner.ts @@ -73,6 +73,7 @@ export function enqueueSeleniumJob(data: SeleniumJobData): string { return runClaimStatusProcessor({ enrichedPayload: data.enrichedPayload, insuranceId: data.insuranceId!, + userId: data.userId, }); } if (jobType === "claim-submit" || jobType === "claim-pre-auth") { @@ -82,6 +83,7 @@ export function enqueueSeleniumJob(data: SeleniumJobData): string { claimId: data.claimId, variant: jobType === "claim-pre-auth" ? "claim-pre-auth" : "claimsubmit", socketId: data.socketId, + userId: data.userId, }); } if (jobType === "ddma-eligibility-check") { diff --git a/apps/Backend/src/queue/processors/bcbsMaEligibilityProcessor.ts b/apps/Backend/src/queue/processors/bcbsMaEligibilityProcessor.ts index cf736274..38fb3b62 100644 --- a/apps/Backend/src/queue/processors/bcbsMaEligibilityProcessor.ts +++ b/apps/Backend/src/queue/processors/bcbsMaEligibilityProcessor.ts @@ -37,7 +37,7 @@ export interface BcbsMaEligibilityProcessorInput { export interface BcbsMaEligibilityProcessorResult { patientUpdateStatus?: string; pdfUploadStatus?: string; - pdfFileId?: number | null; + pdfFileId?: number | string | null; pdfFilename?: string | null; } @@ -50,7 +50,7 @@ async function processBcbsMaResult( seleniumResult: any ): Promise { const output: BcbsMaEligibilityProcessorResult = {}; - let createdPdfFileId: number | null = null; + let createdPdfFileId: number | string | null = null; try { // Prefer names extracted from the BCBS MA results page (Demographic Information section) @@ -133,16 +133,17 @@ async function processBcbsMaResult( } if (pdfBuffer && pdfFilename) { - const groupTitleKey = "ELIGIBILITY_STATUS"; - const groupTitle = "Eligibility Status"; - let group = await storage.findPdfGroupByPatientTitleKey(patient.id, groupTitleKey); - if (!group) group = await storage.createPdfGroup(patient.id, groupTitle, groupTitleKey); - if (!group?.id) throw new Error("PDF group creation failed"); - const created = await storage.createPdfFile(group.id, pdfFilename, pdfBuffer); - if (created && typeof created === "object" && "id" in created) { - createdPdfFileId = Number(created.id); - } - output.pdfUploadStatus = `PDF saved to group: ${group.title}`; + const patientName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() || `Patient ${patient.id}`; + const cloudFile = await storage.savePdfToCloudStorage( + userId, + patient.id, + patientName, + "Eligibility", + pdfFilename, + pdfBuffer + ); + createdPdfFileId = `cloud:${cloudFile.id}`; + output.pdfUploadStatus = "PDF saved to Eligibility folder"; output.pdfFilename = pdfFilename; } diff --git a/apps/Backend/src/queue/processors/ccaClaimProcessor.ts b/apps/Backend/src/queue/processors/ccaClaimProcessor.ts index c801e084..bd7bde5a 100644 --- a/apps/Backend/src/queue/processors/ccaClaimProcessor.ts +++ b/apps/Backend/src/queue/processors/ccaClaimProcessor.ts @@ -79,7 +79,7 @@ export interface CCAClaimProcessorInput { export async function runCCAClaimProcessor( input: CCAClaimProcessorInput, jobId: string -): Promise<{ status: string; claimNumber?: string | null; pdfFileId?: number | null }> { +): Promise<{ status: string; claimNumber?: string | null; pdfFileId?: number | string | null }> { const { enrichedPayload, userId, claimId, socketId } = input; log("cca-claim-processor", "starting Python agent session", { claimId }); @@ -106,19 +106,22 @@ export async function runCCAClaimProcessor( seleniumResult?.pdfFilename || `cca_claim_${claimId ?? "unknown"}_${Date.now()}.pdf`; // Save PDF to patient's Claims document group - let pdfFileId: number | null = null; + let pdfFileId: number | string | null = null; if (pdfBase64 && enrichedPayload?.claim?.patientId) { try { const patientId = Number(enrichedPayload.claim.patientId); const pdfBuffer = Buffer.from(pdfBase64, "base64"); - let group = await storage.findPdfGroupByPatientTitleKey(patientId, "INSURANCE_CLAIM"); - if (!group) { - group = await storage.createPdfGroup(patientId, "Claims", "INSURANCE_CLAIM"); - } - const created = await storage.createPdfFile(group.id!, pdfFilename, pdfBuffer); - if (created && typeof created === "object" && "id" in created) { - pdfFileId = Number((created as any).id); - } + const patient = await storage.getPatient(patientId); + const patientName = patient ? `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() : ""; + const cloudFile = await storage.savePdfToCloudStorage( + userId, + patientId, + patientName || `Patient ${patientId}`, + "Claims", + pdfFilename, + pdfBuffer + ); + pdfFileId = `cloud:${cloudFile.id}`; log("cca-claim-processor", "PDF saved", { pdfFilename, pdfFileId, patientId }); } catch (e: any) { log("cca-claim-processor", "failed to save PDF", { error: e?.message }); diff --git a/apps/Backend/src/queue/processors/ccaEligibilityProcessor.ts b/apps/Backend/src/queue/processors/ccaEligibilityProcessor.ts index c1ea943a..d7acc101 100644 --- a/apps/Backend/src/queue/processors/ccaEligibilityProcessor.ts +++ b/apps/Backend/src/queue/processors/ccaEligibilityProcessor.ts @@ -59,7 +59,7 @@ export interface CCAEligibilityProcessorInput { export interface CCAEligibilityProcessorResult { patientUpdateStatus?: string; pdfUploadStatus?: string; - pdfFileId?: number | null; + pdfFileId?: number | string | null; pdfFilename?: string | null; } @@ -74,7 +74,7 @@ async function processCCAResult( seleniumResult: any ): Promise { const output: CCAEligibilityProcessorResult = {}; - let createdPdfFileId: number | null = null; + let createdPdfFileId: number | string | null = null; try { // 1) Resolve patient name @@ -143,18 +143,17 @@ async function processCCAResult( // 6) Save PDF to patient document group if (pdfBuffer && pdfFilename) { - const groupTitleKey = "ELIGIBILITY_STATUS"; - const groupTitle = "Eligibility Status"; - - let group = await storage.findPdfGroupByPatientTitleKey(patient.id, groupTitleKey); - if (!group) group = await storage.createPdfGroup(patient.id, groupTitle, groupTitleKey); - if (!group?.id) throw new Error("PDF group creation failed"); - - const created = await storage.createPdfFile(group.id, pdfFilename, pdfBuffer); - if (created && typeof created === "object" && "id" in created) { - createdPdfFileId = Number(created.id); - } - output.pdfUploadStatus = `PDF saved to group: ${group.title}`; + const patientName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() || `Patient ${patient.id}`; + const cloudFile = await storage.savePdfToCloudStorage( + userId, + patient.id, + patientName, + "Eligibility", + pdfFilename, + pdfBuffer + ); + createdPdfFileId = `cloud:${cloudFile.id}`; + output.pdfUploadStatus = "PDF saved to Eligibility folder"; output.pdfFilename = pdfFilename; } diff --git a/apps/Backend/src/queue/processors/ccaPreAuthProcessor.ts b/apps/Backend/src/queue/processors/ccaPreAuthProcessor.ts index 93f7c06b..6a4ef5d5 100644 --- a/apps/Backend/src/queue/processors/ccaPreAuthProcessor.ts +++ b/apps/Backend/src/queue/processors/ccaPreAuthProcessor.ts @@ -73,7 +73,7 @@ export interface CCAPreAuthProcessorInput { export async function runCCAPreAuthProcessor( input: CCAPreAuthProcessorInput, jobId: string -): Promise<{ status: string; authNumber?: string | null; pdfFileId?: number | null }> { +): Promise<{ status: string; authNumber?: string | null; pdfFileId?: number | string | null }> { const { enrichedPayload, userId, claimId, socketId } = input; log("cca-preauth-processor", "starting Python agent session", { claimId }); @@ -110,19 +110,22 @@ export async function runCCAPreAuthProcessor( } // Save PDF to patient's PreAuth document group - let pdfFileId: number | null = null; + let pdfFileId: number | string | null = null; if (pdfBase64 && enrichedPayload?.claim?.patientId) { try { const patientId = Number(enrichedPayload.claim.patientId); const pdfBuffer = Buffer.from(pdfBase64, "base64"); - let group = await storage.findPdfGroupByPatientTitleKey(patientId, "INSURANCE_CLAIM_PREAUTH"); - if (!group) { - group = await storage.createPdfGroup(patientId, "PreAuth", "INSURANCE_CLAIM_PREAUTH"); - } - const created = await storage.createPdfFile(group.id!, pdfFilename, pdfBuffer); - if (created && typeof created === "object" && "id" in created) { - pdfFileId = Number((created as any).id); - } + const patient = await storage.getPatient(patientId); + const patientName = patient ? `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() : ""; + const cloudFile = await storage.savePdfToCloudStorage( + userId, + patientId, + patientName || `Patient ${patientId}`, + "PreAuth", + pdfFilename, + pdfBuffer + ); + pdfFileId = `cloud:${cloudFile.id}`; log("cca-preauth-processor", "PDF saved", { pdfFilename, pdfFileId, patientId }); } catch (e: any) { log("cca-preauth-processor", "failed to save PDF", { error: e?.message }); diff --git a/apps/Backend/src/queue/processors/claimStatusProcessor.ts b/apps/Backend/src/queue/processors/claimStatusProcessor.ts index 9b43d373..24cd966e 100644 --- a/apps/Backend/src/queue/processors/claimStatusProcessor.ts +++ b/apps/Backend/src/queue/processors/claimStatusProcessor.ts @@ -15,17 +15,18 @@ import { export interface ClaimStatusProcessorInput { enrichedPayload: any; insuranceId: string; // memberId used to look up the patient + userId: number; } export interface ClaimStatusProcessorResult { pdfUploadStatus?: string; - pdfFileId?: number | null; + pdfFileId?: number | string | null; } export async function runClaimStatusProcessor( input: ClaimStatusProcessorInput ): Promise { - const { enrichedPayload, insuranceId } = input; + const { enrichedPayload, insuranceId, userId } = input; // 1) Call the Python service synchronously (BullMQ worker handles async) const result = await callPythonSync("/claim-status-check", { @@ -61,22 +62,19 @@ export async function runClaimStatusProcessor( } if (pdfBuffer && generatedPdfPath) { - const groupTitleKey = "CLAIM_STATUS"; - const groupTitle = "Claim Status"; - - let group = await storage.findPdfGroupByPatientTitleKey(patient.id, groupTitleKey); - if (!group) group = await storage.createPdfGroup(patient.id, groupTitle, groupTitleKey); - if (!group?.id) throw new Error("PDF group creation failed"); - + const patientName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim(); const basename = path.basename(generatedPdfPath); - const created = await storage.createPdfFile(group.id, basename, pdfBuffer); + const cloudFile = await storage.savePdfToCloudStorage( + userId, + patient.id, + patientName || `Patient ${patient.id}`, + "Claim Status", + basename, + pdfBuffer + ); + const createdPdfFileId: number | string | null = `cloud:${cloudFile.id}`; - let createdPdfFileId: number | null = null; - if (created && typeof created === "object" && "id" in created) { - createdPdfFileId = Number(created.id); - } - - outputResult.pdfUploadStatus = `PDF saved to group: ${group.title}`; + outputResult.pdfUploadStatus = `PDF saved to Claim Status`; outputResult.pdfFileId = createdPdfFileId; } } else { diff --git a/apps/Backend/src/queue/processors/claimSubmitProcessor.ts b/apps/Backend/src/queue/processors/claimSubmitProcessor.ts index 67a24d25..f076d1ac 100644 --- a/apps/Backend/src/queue/processors/claimSubmitProcessor.ts +++ b/apps/Backend/src/queue/processors/claimSubmitProcessor.ts @@ -15,6 +15,7 @@ export interface ClaimSubmitProcessorInput { variant?: "claimsubmit" | "claim-pre-auth"; /** When set, the frontend socket listener will handle the PDF download — skip auto-save */ socketId?: string; + userId: number; } export interface ClaimSubmitProcessorResult { @@ -28,7 +29,8 @@ export interface ClaimSubmitProcessorResult { async function savePdfFromSelenium( pdf_url: string, patientId: number, - variant: "claimsubmit" | "claim-pre-auth" + variant: "claimsubmit" | "claim-pre-auth", + userId: number ) { try { const filename = path.basename(new URL(pdf_url).pathname); @@ -37,15 +39,19 @@ async function savePdfFromSelenium( const resp = await axios.get(localUrl, { responseType: "arraybuffer", timeout: 30000 }); - const groupTitleKey = variant === "claim-pre-auth" ? "INSURANCE_CLAIM_PREAUTH" : "INSURANCE_CLAIM"; - const groupTitle = variant === "claim-pre-auth" ? "Preauth" : "Claims"; + const category = variant === "claim-pre-auth" ? "PreAuth" : "Claims"; - let group = await storage.findPdfGroupByPatientTitleKey(patientId, groupTitleKey); - if (!group) { - group = await storage.createPdfGroup(patientId, groupTitle, groupTitleKey); - } + const patient = await storage.getPatient(patientId); + const patientName = patient ? `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() : ""; - await storage.createPdfFile(group.id!, filename, resp.data); + await storage.savePdfToCloudStorage( + userId, + patientId, + patientName || `Patient ${patientId}`, + category, + filename, + Buffer.from(resp.data) + ); console.log(`[claimSubmitProcessor] PDF saved for patient ${patientId}: ${filename}`); } catch (err: any) { // Non-fatal — claim was submitted; just log the PDF failure @@ -56,7 +62,7 @@ async function savePdfFromSelenium( export async function runClaimSubmitProcessor( input: ClaimSubmitProcessorInput ): Promise { - const { enrichedPayload, files, claimId } = input; + const { enrichedPayload, files, claimId, userId } = input; const variant = input.variant ?? "claimsubmit"; // Build the same payload shape the Python /claimsubmit endpoint expects @@ -97,7 +103,8 @@ export async function runClaimSubmitProcessor( await savePdfFromSelenium( result.pdf_url, Number(enrichedPayload.patientId), - variant + variant, + userId ); } diff --git a/apps/Backend/src/queue/processors/cmspEligibilityHistoryRemainingProcessor.ts b/apps/Backend/src/queue/processors/cmspEligibilityHistoryRemainingProcessor.ts index c1c82336..05c65899 100644 --- a/apps/Backend/src/queue/processors/cmspEligibilityHistoryRemainingProcessor.ts +++ b/apps/Backend/src/queue/processors/cmspEligibilityHistoryRemainingProcessor.ts @@ -26,13 +26,13 @@ export interface CmspEligibilityHistoryRemainingProcessorInput { export interface CmspEligibilityHistoryRemainingProcessorResult { patientUpdateStatus?: string; pdfUploadStatus?: string; - pdfFileId?: number | null; + pdfFileId?: number | string | null; pdfFilename?: string | null; - memberDetailsPdfFileId?: number | null; + memberDetailsPdfFileId?: number | string | null; memberDetailsPdfFilename?: string | null; - historyPdfFileId?: number | null; + historyPdfFileId?: number | string | null; historyPdfFilename?: string | null; - accumulatorPdfFileId?: number | null; + accumulatorPdfFileId?: number | string | null; accumulatorPdfFilename?: string | null; } @@ -108,20 +108,20 @@ export async function runCmspEligibilityHistoryRemainingProcessor( await storage.updatePatient(patient.id, updates); outputResult.patientUpdateStatus = `Patient status updated to ${newStatus}`; - // Helper: save a single PDF to the patient's ELIGIBILITY_STATUS group - const saveToGroup = async (pdfPath: string): Promise => { + // Helper: save a single PDF to the patient's Cloud Storage Eligibility folder + const patientName = `${patient!.firstName ?? ""} ${patient!.lastName ?? ""}`.trim() || `Patient ${patient!.id}`; + const saveToGroup = async (pdfPath: string): Promise => { try { const buffer = await fs.readFile(pdfPath); - const groupTitleKey = "ELIGIBILITY_STATUS"; - const groupTitle = "Eligibility Status"; - let group = await storage.findPdfGroupByPatientTitleKey(patient!.id, groupTitleKey); - if (!group) group = await storage.createPdfGroup(patient!.id, groupTitle, groupTitleKey); - if (!group?.id) throw new Error("PDF group creation failed"); - const created = await storage.createPdfFile(group.id, path.basename(pdfPath), buffer); - if (created && typeof created === "object" && "id" in created) { - return Number(created.id); - } - return null; + const cloudFile = await storage.savePdfToCloudStorage( + userId, + patient!.id, + patientName, + "Eligibility", + path.basename(pdfPath), + buffer + ); + return `cloud:${cloudFile.id}`; } catch (e: any) { console.error("[cmspProcessor] saveToGroup failed:", e.message); return null; diff --git a/apps/Backend/src/queue/processors/ddmaClaimProcessor.ts b/apps/Backend/src/queue/processors/ddmaClaimProcessor.ts index 0e54e4ad..4e72ade4 100644 --- a/apps/Backend/src/queue/processors/ddmaClaimProcessor.ts +++ b/apps/Backend/src/queue/processors/ddmaClaimProcessor.ts @@ -88,15 +88,22 @@ async function pollUntilDone( throw new Error(`DDMA claim polling exhausted all attempts for session ${sessionId}`); } -async function savePdfFromSelenium(pdf_url: string, patientId: number) { +async function savePdfFromSelenium(pdf_url: string, patientId: number, userId: number) { try { const filename = path.basename(new URL(pdf_url).pathname); const seleniumPort = process.env.SELENIUM_PORT || "5002"; const localUrl = `http://localhost:${seleniumPort}/downloads/${filename}`; const resp = await axios.get(localUrl, { responseType: "arraybuffer", timeout: 30000 }); - let group = await storage.findPdfGroupByPatientTitleKey(patientId, "INSURANCE_CLAIM"); - if (!group) group = await storage.createPdfGroup(patientId, "Claims", "INSURANCE_CLAIM"); - await storage.createPdfFile(group.id!, filename, resp.data); + const patient = await storage.getPatient(patientId); + const patientName = patient ? `${patient.firstName} ${patient.lastName}` : `Patient ${patientId}`; + await storage.savePdfToCloudStorage( + userId, + patientId, + patientName, + "Claims", + filename, + Buffer.from(resp.data) + ); log("ddma-claim-processor", "PDF saved", { patientId, filename }); } catch (err: any) { log("ddma-claim-processor", "failed to save PDF (non-fatal)", { error: err?.message ?? err }); @@ -160,7 +167,7 @@ export async function runDDMAClaimProcessor( if (pdf_url && !socketId) { const claim = claimId ? await storage.getClaim(claimId).catch(() => null) : null; const patientId = claim?.patientId ?? enrichedPayload?.claim?.patientId ?? enrichedPayload?.patientId; - if (patientId) await savePdfFromSelenium(pdf_url, Number(patientId)); + if (patientId) await savePdfFromSelenium(pdf_url, Number(patientId), userId); } emitToSocket(socketId, "selenium:ddma_claim_completed", { diff --git a/apps/Backend/src/queue/processors/ddmaEligibilityProcessor.ts b/apps/Backend/src/queue/processors/ddmaEligibilityProcessor.ts index 2d56d56a..b316101c 100644 --- a/apps/Backend/src/queue/processors/ddmaEligibilityProcessor.ts +++ b/apps/Backend/src/queue/processors/ddmaEligibilityProcessor.ts @@ -66,7 +66,7 @@ export interface DdmaEligibilityProcessorInput { export interface DdmaEligibilityProcessorResult { patientUpdateStatus?: string; pdfUploadStatus?: string; - pdfFileId?: number | null; + pdfFileId?: number | string | null; pdfFilename?: string | null; } @@ -81,7 +81,7 @@ async function processDdmaResult( seleniumResult: any ): Promise { const output: DdmaEligibilityProcessorResult = {}; - let createdPdfFileId: number | null = null; + let createdPdfFileId: number | string | null = null; try { // 1) Resolve patient name (prefer selenium extraction → form data) @@ -185,18 +185,17 @@ async function processDdmaResult( // 6) Save PDF to patient document group if (pdfBuffer && pdfFilename) { - const groupTitleKey = "ELIGIBILITY_STATUS"; - const groupTitle = "Eligibility Status"; - - let group = await storage.findPdfGroupByPatientTitleKey(patient.id, groupTitleKey); - if (!group) group = await storage.createPdfGroup(patient.id, groupTitle, groupTitleKey); - if (!group?.id) throw new Error("PDF group creation failed"); - - const created = await storage.createPdfFile(group.id, pdfFilename, pdfBuffer); - if (created && typeof created === "object" && "id" in created) { - createdPdfFileId = Number(created.id); - } - output.pdfUploadStatus = `PDF saved to group: ${group.title}`; + const patientName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() || `Patient ${patient.id}`; + const cloudFile = await storage.savePdfToCloudStorage( + userId, + patient.id, + patientName, + "Eligibility", + pdfFilename, + pdfBuffer + ); + createdPdfFileId = `cloud:${cloudFile.id}`; + output.pdfUploadStatus = "PDF saved to Eligibility folder"; output.pdfFilename = pdfFilename; } diff --git a/apps/Backend/src/queue/processors/deltaInsEligibilityProcessor.ts b/apps/Backend/src/queue/processors/deltaInsEligibilityProcessor.ts index a5fe2d2c..c2d9215e 100644 --- a/apps/Backend/src/queue/processors/deltaInsEligibilityProcessor.ts +++ b/apps/Backend/src/queue/processors/deltaInsEligibilityProcessor.ts @@ -56,7 +56,7 @@ export interface DeltaInsEligibilityProcessorInput { export interface DeltaInsEligibilityProcessorResult { patientUpdateStatus?: string; pdfUploadStatus?: string; - pdfFileId?: number | null; + pdfFileId?: number | string | null; pdfFilename?: string | null; } @@ -71,7 +71,7 @@ async function processDeltaInsResult( seleniumResult: any ): Promise { const output: DeltaInsEligibilityProcessorResult = {}; - let createdPdfFileId: number | null = null; + let createdPdfFileId: number | string | null = null; try { // 1) Resolve patient name @@ -132,18 +132,17 @@ async function processDeltaInsResult( // 6) Save PDF to patient document group if (pdfBuffer && pdfFilename) { - const groupTitleKey = "ELIGIBILITY_STATUS"; - const groupTitle = "Eligibility Status"; - - let group = await storage.findPdfGroupByPatientTitleKey(patient.id, groupTitleKey); - if (!group) group = await storage.createPdfGroup(patient.id, groupTitle, groupTitleKey); - if (!group?.id) throw new Error("PDF group creation failed"); - - const created = await storage.createPdfFile(group.id, pdfFilename, pdfBuffer); - if (created && typeof created === "object" && "id" in created) { - createdPdfFileId = Number(created.id); - } - output.pdfUploadStatus = `PDF saved to group: ${group.title}`; + const patientName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() || `Patient ${patient.id}`; + const cloudFile = await storage.savePdfToCloudStorage( + userId, + patient.id, + patientName, + "Eligibility", + pdfFilename, + pdfBuffer + ); + createdPdfFileId = `cloud:${cloudFile.id}`; + output.pdfUploadStatus = "PDF saved to Eligibility folder"; output.pdfFilename = pdfFilename; } diff --git a/apps/Backend/src/queue/processors/dentaQuestEligibilityProcessor.ts b/apps/Backend/src/queue/processors/dentaQuestEligibilityProcessor.ts index ecce9f36..44925d13 100644 --- a/apps/Backend/src/queue/processors/dentaQuestEligibilityProcessor.ts +++ b/apps/Backend/src/queue/processors/dentaQuestEligibilityProcessor.ts @@ -44,7 +44,7 @@ export interface DentaQuestEligibilityProcessorInput { export interface DentaQuestEligibilityProcessorResult { patientUpdateStatus?: string; pdfUploadStatus?: string; - pdfFileId?: number | null; + pdfFileId?: number | string | null; pdfFilename?: string | null; } @@ -57,7 +57,7 @@ async function processDentaQuestResult( seleniumResult: any ): Promise { const output: DentaQuestEligibilityProcessorResult = {}; - let createdPdfFileId: number | null = null; + let createdPdfFileId: number | string | null = null; try { const rawName = @@ -130,18 +130,17 @@ async function processDentaQuestResult( } if (pdfBuffer && pdfFilename) { - const groupTitleKey = "ELIGIBILITY_STATUS"; - const groupTitle = "Eligibility Status"; - - let group = await storage.findPdfGroupByPatientTitleKey(patient.id, groupTitleKey); - if (!group) group = await storage.createPdfGroup(patient.id, groupTitle, groupTitleKey); - if (!group?.id) throw new Error("PDF group creation failed"); - - const created = await storage.createPdfFile(group.id, pdfFilename, pdfBuffer); - if (created && typeof created === "object" && "id" in created) { - createdPdfFileId = Number(created.id); - } - output.pdfUploadStatus = `PDF saved to group: ${group.title}`; + const patientName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() || `Patient ${patient.id}`; + const cloudFile = await storage.savePdfToCloudStorage( + userId, + patient.id, + patientName, + "Eligibility", + pdfFilename, + pdfBuffer + ); + createdPdfFileId = `cloud:${cloudFile.id}`; + output.pdfUploadStatus = "PDF saved to Eligibility folder"; output.pdfFilename = pdfFilename; } diff --git a/apps/Backend/src/queue/processors/eligibilityHistoryProcessor.ts b/apps/Backend/src/queue/processors/eligibilityHistoryProcessor.ts index 2e4ede8b..e888e24b 100644 --- a/apps/Backend/src/queue/processors/eligibilityHistoryProcessor.ts +++ b/apps/Backend/src/queue/processors/eligibilityHistoryProcessor.ts @@ -26,11 +26,11 @@ export interface EligibilityHistoryProcessorInput { export interface EligibilityHistoryProcessorResult { patientUpdateStatus?: string; pdfUploadStatus?: string; - pdfFileId?: number | null; + pdfFileId?: number | string | null; pdfFilename?: string | null; - memberDetailsPdfFileId?: number | null; + memberDetailsPdfFileId?: number | string | null; memberDetailsPdfFilename?: string | null; - historyPdfFileId?: number | null; + historyPdfFileId?: number | string | null; historyPdfFilename?: string | null; } @@ -106,20 +106,20 @@ export async function runEligibilityHistoryProcessor( await storage.updatePatient(patient.id, updates); outputResult.patientUpdateStatus = `Patient status updated to ${newStatus}`; - // Helper: save a PDF buffer to the patient's ELIGIBILITY_STATUS group - const saveToGroup = async (pdfPath: string): Promise => { + // Helper: save a PDF buffer to the patient's Cloud Storage Eligibility folder + const patientName = `${patient!.firstName ?? ""} ${patient!.lastName ?? ""}`.trim() || `Patient ${patient!.id}`; + const saveToGroup = async (pdfPath: string): Promise => { try { const buffer = await fs.readFile(pdfPath); - const groupTitleKey = "ELIGIBILITY_STATUS"; - const groupTitle = "Eligibility Status"; - let group = await storage.findPdfGroupByPatientTitleKey(patient!.id, groupTitleKey); - if (!group) group = await storage.createPdfGroup(patient!.id, groupTitle, groupTitleKey); - if (!group?.id) throw new Error("PDF group creation failed"); - const created = await storage.createPdfFile(group.id, path.basename(pdfPath), buffer); - if (created && typeof created === "object" && "id" in created) { - return Number(created.id); - } - return null; + const cloudFile = await storage.savePdfToCloudStorage( + userId, + patient!.id, + patientName, + "Eligibility", + path.basename(pdfPath), + buffer + ); + return `cloud:${cloudFile.id}`; } catch (e: any) { console.error("[eligibilityHistoryProcessor] saveToGroup failed:", e.message); return null; @@ -127,7 +127,7 @@ export async function runEligibilityHistoryProcessor( }; // Save eligibility PDF - let eligibilityPdfFileId: number | null = null; + let eligibilityPdfFileId: number | string | null = null; if (seleniumResult.pdf_path?.endsWith(".pdf")) { eligibilityPdfFileId = await saveToGroup(seleniumResult.pdf_path); outputResult.pdfUploadStatus = eligibilityPdfFileId @@ -146,7 +146,7 @@ export async function runEligibilityHistoryProcessor( } // Save history PDF - let historyPdfFileId: number | null = null; + let historyPdfFileId: number | string | null = null; if (seleniumResult.history_pdf_path?.endsWith(".pdf")) { historyPdfFileId = await saveToGroup(seleniumResult.history_pdf_path); outputResult.historyPdfFilename = path.basename(seleniumResult.history_pdf_path); diff --git a/apps/Backend/src/queue/processors/eligibilityProcessor.ts b/apps/Backend/src/queue/processors/eligibilityProcessor.ts index 9396ef12..e5598c0f 100644 --- a/apps/Backend/src/queue/processors/eligibilityProcessor.ts +++ b/apps/Backend/src/queue/processors/eligibilityProcessor.ts @@ -29,7 +29,7 @@ export interface EligibilityProcessorInput { export interface EligibilityProcessorResult { patientUpdateStatus?: string; pdfUploadStatus?: string; - pdfFileId?: number | null; + pdfFileId?: number | string | null; } export async function runEligibilityProcessor( @@ -122,27 +122,23 @@ export async function runEligibilityProcessor( outputResult.patientUpdateStatus = `Patient status updated to ${newStatus}`; // 6) Save PDF - let createdPdfFileId: number | null = null; + let createdPdfFileId: number | string | null = null; if (seleniumResult.pdf_path?.endsWith(".pdf")) { try { const pdfBuffer = await fs.readFile(seleniumResult.pdf_path); - const groupTitleKey = "ELIGIBILITY_STATUS"; - const groupTitle = "Eligibility Status"; + const patientName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() || `Patient ${patient.id}`; - let group = await storage.findPdfGroupByPatientTitleKey(patient.id, groupTitleKey); - if (!group) group = await storage.createPdfGroup(patient.id, groupTitle, groupTitleKey); - if (!group?.id) throw new Error("PDF group creation failed"); - - const created = await storage.createPdfFile( - group.id, + const cloudFile = await storage.savePdfToCloudStorage( + userId, + patient.id, + patientName, + "Eligibility", path.basename(seleniumResult.pdf_path), pdfBuffer ); - if (created && typeof created === "object" && "id" in created) { - createdPdfFileId = Number(created.id); - } - outputResult.pdfUploadStatus = `PDF saved to group: ${group.title}`; + createdPdfFileId = `cloud:${cloudFile.id}`; + outputResult.pdfUploadStatus = "PDF saved to Eligibility folder"; } catch (e: any) { outputResult.pdfUploadStatus = `PDF upload failed: ${e.message}`; } diff --git a/apps/Backend/src/queue/processors/tuftsSCOClaimProcessor.ts b/apps/Backend/src/queue/processors/tuftsSCOClaimProcessor.ts index a2825ced..50d6ba81 100644 --- a/apps/Backend/src/queue/processors/tuftsSCOClaimProcessor.ts +++ b/apps/Backend/src/queue/processors/tuftsSCOClaimProcessor.ts @@ -87,15 +87,22 @@ async function pollUntilDone( throw new Error(`Tufts SCO claim polling exhausted all attempts for session ${sessionId}`); } -async function savePdfFromSelenium(pdf_url: string, patientId: number) { +async function savePdfFromSelenium(pdf_url: string, patientId: number, userId: number) { try { const filename = path.basename(new URL(pdf_url).pathname); const seleniumPort = process.env.SELENIUM_PORT || "5002"; const localUrl = `http://localhost:${seleniumPort}/downloads/${filename}`; const resp = await axios.get(localUrl, { responseType: "arraybuffer", timeout: 30000 }); - let group = await storage.findPdfGroupByPatientTitleKey(patientId, "INSURANCE_CLAIM"); - if (!group) group = await storage.createPdfGroup(patientId, "Claims", "INSURANCE_CLAIM"); - await storage.createPdfFile(group.id!, filename, resp.data); + const patient = await storage.getPatient(patientId); + const patientName = patient ? `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() : ""; + await storage.savePdfToCloudStorage( + userId, + patientId, + patientName || `Patient ${patientId}`, + "Claims", + filename, + Buffer.from(resp.data) + ); log("tuftssco-claim-processor", "PDF saved", { patientId, filename }); } catch (err: any) { log("tuftssco-claim-processor", "failed to save PDF (non-fatal)", { error: err?.message ?? err }); @@ -151,7 +158,7 @@ export async function runTuftsSCOClaimProcessor( if (pdf_url && !socketId) { const claim = claimId ? await storage.getClaim(claimId).catch(() => null) : null; const patientId = claim?.patientId ?? enrichedPayload?.claim?.patientId ?? enrichedPayload?.patientId; - if (patientId) await savePdfFromSelenium(pdf_url, Number(patientId)); + if (patientId) await savePdfFromSelenium(pdf_url, Number(patientId), userId); } emitToSocket(socketId, "selenium:tuftssco_claim_completed", { diff --git a/apps/Backend/src/queue/processors/tuftsSCOPreAuthProcessor.ts b/apps/Backend/src/queue/processors/tuftsSCOPreAuthProcessor.ts index 70406449..412c62f7 100644 --- a/apps/Backend/src/queue/processors/tuftsSCOPreAuthProcessor.ts +++ b/apps/Backend/src/queue/processors/tuftsSCOPreAuthProcessor.ts @@ -88,16 +88,21 @@ async function pollUntilDone( throw new Error(`Tufts SCO preauth polling exhausted all attempts for session ${sessionId}`); } -async function savePdfFromSelenium(pdf_url: string, patientId: number) { +async function savePdfFromSelenium(pdf_url: string, patientId: number, userId: number) { try { const filename = path.basename(new URL(pdf_url).pathname); const resp = await axios.get(pdf_url, { responseType: "arraybuffer", timeout: 30000 }); - let group = await storage.findPdfGroupByPatientTitleKey(patientId, "INSURANCE_CLAIM_PREAUTH"); - if (!group) { - group = await storage.createPdfGroup(patientId, "PreAuth", "INSURANCE_CLAIM_PREAUTH"); - } - await storage.createPdfFile(group.id!, filename, resp.data); + const patient = await storage.getPatient(patientId); + const patientName = patient ? `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() : ""; + await storage.savePdfToCloudStorage( + userId, + patientId, + patientName || `Patient ${patientId}`, + "PreAuth", + filename, + Buffer.from(resp.data) + ); log("tuftssco-preauth-processor", "PDF saved", { patientId, filename }); } catch (err: any) { log("tuftssco-preauth-processor", "failed to save PDF (non-fatal)", { error: err?.message ?? err }); @@ -158,7 +163,7 @@ export async function runTuftsSCOPreAuthProcessor( if (pdf_url && !socketId) { const claim = claimId ? await storage.getClaim(claimId).catch(() => null) : null; const patientId = claim?.patientId ?? enrichedPayload?.claim?.patientId ?? enrichedPayload?.patientId; - if (patientId) await savePdfFromSelenium(pdf_url, Number(patientId)); + if (patientId) await savePdfFromSelenium(pdf_url, Number(patientId), userId); } emitToSocket(socketId, "selenium:tuftssco_preauth_completed", { diff --git a/apps/Backend/src/queue/processors/unitedDHClaimProcessor.ts b/apps/Backend/src/queue/processors/unitedDHClaimProcessor.ts index 00799614..8f1efc75 100644 --- a/apps/Backend/src/queue/processors/unitedDHClaimProcessor.ts +++ b/apps/Backend/src/queue/processors/unitedDHClaimProcessor.ts @@ -99,18 +99,23 @@ async function pollUntilDone( throw new Error(`UnitedDH claim polling exhausted all attempts for session ${sessionId}`); } -async function savePdfFromSelenium(pdf_url: string, patientId: number) { +async function savePdfFromSelenium(pdf_url: string, patientId: number, userId: number) { try { const filename = path.basename(new URL(pdf_url).pathname); const seleniumPort = process.env.SELENIUM_PORT || "5002"; const localUrl = `http://localhost:${seleniumPort}/downloads/${filename}`; const resp = await axios.get(localUrl, { responseType: "arraybuffer", timeout: 30000 }); - let group = await storage.findPdfGroupByPatientTitleKey(patientId, "INSURANCE_CLAIM"); - if (!group) { - group = await storage.createPdfGroup(patientId, "Claims", "INSURANCE_CLAIM"); - } - await storage.createPdfFile(group.id!, filename, resp.data); + const patient = await storage.getPatient(patientId); + const patientName = patient ? `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() : ""; + await storage.savePdfToCloudStorage( + userId, + patientId, + patientName || `Patient ${patientId}`, + "Claims", + filename, + Buffer.from(resp.data) + ); log("uniteddh-claim-processor", "PDF saved", { patientId, filename }); } catch (err: any) { log("uniteddh-claim-processor", "failed to save PDF (non-fatal)", { error: err?.message ?? err }); @@ -173,7 +178,7 @@ export async function runUnitedDHClaimProcessor( if (pdf_url && !socketId) { const claim = claimId ? await storage.getClaim(claimId).catch(() => null) : null; const patientId = claim?.patientId ?? enrichedPayload?.claim?.patientId ?? enrichedPayload?.patientId; - if (patientId) await savePdfFromSelenium(pdf_url, Number(patientId)); + if (patientId) await savePdfFromSelenium(pdf_url, Number(patientId), userId); } emitToSocket(socketId, "selenium:uniteddh_claim_completed", { diff --git a/apps/Backend/src/queue/processors/unitedDHPreAuthProcessor.ts b/apps/Backend/src/queue/processors/unitedDHPreAuthProcessor.ts index bd131936..0886e0d3 100644 --- a/apps/Backend/src/queue/processors/unitedDHPreAuthProcessor.ts +++ b/apps/Backend/src/queue/processors/unitedDHPreAuthProcessor.ts @@ -98,18 +98,23 @@ async function pollUntilDone( throw new Error(`UnitedDH preauth polling exhausted all attempts for session ${sessionId}`); } -async function savePdfFromSelenium(pdf_url: string, patientId: number) { +async function savePdfFromSelenium(pdf_url: string, patientId: number, userId: number) { try { const filename = path.basename(new URL(pdf_url).pathname); const seleniumPort = process.env.SELENIUM_PORT || "5002"; const localUrl = `http://localhost:${seleniumPort}/downloads/${filename}`; const resp = await axios.get(localUrl, { responseType: "arraybuffer", timeout: 30000 }); - let group = await storage.findPdfGroupByPatientTitleKey(patientId, "INSURANCE_CLAIM_PREAUTH"); - if (!group) { - group = await storage.createPdfGroup(patientId, "PreAuth", "INSURANCE_CLAIM_PREAUTH"); - } - await storage.createPdfFile(group.id!, filename, resp.data); + const patient = await storage.getPatient(patientId); + const patientName = patient ? `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() : ""; + await storage.savePdfToCloudStorage( + userId, + patientId, + patientName || `Patient ${patientId}`, + "PreAuth", + filename, + Buffer.from(resp.data) + ); log("uniteddh-preauth-processor", "PDF saved", { patientId, filename }); } catch (err: any) { log("uniteddh-preauth-processor", "failed to save PDF (non-fatal)", { error: err?.message ?? err }); @@ -171,7 +176,7 @@ export async function runUnitedDHPreAuthProcessor( if (pdf_url && !socketId) { const claim = claimId ? await storage.getClaim(claimId).catch(() => null) : null; const patientId = claim?.patientId ?? enrichedPayload?.claim?.patientId ?? enrichedPayload?.patientId; - if (patientId) await savePdfFromSelenium(pdf_url, Number(patientId)); + if (patientId) await savePdfFromSelenium(pdf_url, Number(patientId), userId); } emitToSocket(socketId, "selenium:uniteddh_preauth_completed", { diff --git a/apps/Backend/src/queue/processors/unitedSCOEligibilityProcessor.ts b/apps/Backend/src/queue/processors/unitedSCOEligibilityProcessor.ts index 4a0f6839..a78831e1 100644 --- a/apps/Backend/src/queue/processors/unitedSCOEligibilityProcessor.ts +++ b/apps/Backend/src/queue/processors/unitedSCOEligibilityProcessor.ts @@ -58,7 +58,7 @@ export interface UnitedSCOEligibilityProcessorInput { export interface UnitedSCOEligibilityProcessorResult { patientUpdateStatus?: string; pdfUploadStatus?: string; - pdfFileId?: number | null; + pdfFileId?: number | string | null; pdfFilename?: string | null; } @@ -73,7 +73,7 @@ async function processUnitedSCOResult( seleniumResult: any ): Promise { const output: UnitedSCOEligibilityProcessorResult = {}; - let createdPdfFileId: number | null = null; + let createdPdfFileId: number | string | null = null; try { // 1) Resolve patient name @@ -155,18 +155,17 @@ async function processUnitedSCOResult( // 6) Save PDF to patient document group if (pdfBuffer && pdfFilename) { - const groupTitleKey = "ELIGIBILITY_STATUS"; - const groupTitle = "Eligibility Status"; - - let group = await storage.findPdfGroupByPatientTitleKey(patient.id, groupTitleKey); - if (!group) group = await storage.createPdfGroup(patient.id, groupTitle, groupTitleKey); - if (!group?.id) throw new Error("PDF group creation failed"); - - const created = await storage.createPdfFile(group.id, pdfFilename, pdfBuffer); - if (created && typeof created === "object" && "id" in created) { - createdPdfFileId = Number(created.id); - } - output.pdfUploadStatus = `PDF saved to group: ${group.title}`; + const patientName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() || `Patient ${patient.id}`; + const cloudFile = await storage.savePdfToCloudStorage( + userId, + patient.id, + patientName, + "Eligibility", + pdfFilename, + pdfBuffer + ); + createdPdfFileId = `cloud:${cloudFile.id}`; + output.pdfUploadStatus = "PDF saved to Eligibility folder"; output.pdfFilename = pdfFilename; } diff --git a/apps/Backend/src/queue/workers/seleniumWorker.ts b/apps/Backend/src/queue/workers/seleniumWorker.ts index 7a9bc84a..2316d54a 100644 --- a/apps/Backend/src/queue/workers/seleniumWorker.ts +++ b/apps/Backend/src/queue/workers/seleniumWorker.ts @@ -50,6 +50,7 @@ async function processSeleniumJob(job: Job) { result = await runClaimStatusProcessor({ enrichedPayload, insuranceId: job.data.insuranceId!, + userId, }); } else if (jobType === "claim-submit") { result = await runClaimSubmitProcessor({ @@ -58,6 +59,7 @@ async function processSeleniumJob(job: Job) { claimId: job.data.claimId, variant: "claimsubmit", socketId, + userId, }); } else if (jobType === "claim-pre-auth") { result = await runClaimSubmitProcessor({ @@ -66,6 +68,7 @@ async function processSeleniumJob(job: Job) { claimId: job.data.claimId, variant: "claim-pre-auth", socketId, + userId, }); } else { throw new Error(`Unknown selenium jobType: ${jobType}`); diff --git a/apps/Backend/src/routes/claims.ts b/apps/Backend/src/routes/claims.ts index 24706c9f..4c6c5950 100755 --- a/apps/Backend/src/routes/claims.ts +++ b/apps/Backend/src/routes/claims.ts @@ -113,6 +113,11 @@ router.post( try { const folder = await storage.getOrCreatePatientFolder(req.user.id, patientId, patientName); + const attachmentsFolder = await storage.getOrCreateSubfolder( + req.user.id, + (folder as any).id, + "Attachments" + ); const result: { filename: string; mimeType: string; filePath: string }[] = []; for (const file of files) { @@ -126,14 +131,14 @@ router.post( file.buffer ); - // Save to cloud storage patient folder (Cloud Storage page) + // Save to cloud storage patient's Attachments subfolder (Cloud Storage page) const cloudFile = await storage.initializeFileUpload( req.user.id, file.originalname, file.mimetype, BigInt(file.size), 1, - (folder as any).id + (attachmentsFolder as any).id ); await storage.appendFileChunk((cloudFile as any).id, 0, file.buffer); const finalized = await storage.finalizeFileUpload((cloudFile as any).id); @@ -337,36 +342,28 @@ router.post( ); } - const GROUP_TITLES: Record = { + const CLOUD_CATEGORIES: Record = { INSURANCE_CLAIM: "Claims", - INSURANCE_CLAIM_PREAUTH: "Preauth", + INSURANCE_CLAIM_PREAUTH: "PreAuth", }; - const groupTitle = GROUP_TITLES[groupTitleKey]; + const cloudCategory = CLOUD_CATEGORIES[groupTitleKey]; - // ✅ Find or create PDF group for this claim - let group = await storage.findPdfGroupByPatientTitleKey( + const patient = await storage.getPatient(parsedPatientId); + const patientName = patient + ? `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() + : ""; + + // ✅ Save PDF file into the patient's Cloud Storage category subfolder + const cloudFile = await storage.savePdfToCloudStorage( + req.user.id, parsedPatientId, - groupTitleKey + patientName || `Patient ${parsedPatientId}`, + cloudCategory, + filename, + Buffer.from(pdfResponse.data) ); - if (!group) { - group = await storage.createPdfGroup( - parsedPatientId, - groupTitle, - groupTitleKey - ); - } - - // ✅ Save PDF file into that group - const created = await storage.createPdfFile(group.id!, filename, pdfResponse.data); - - // Extract the PDF file ID for opening the viewer - let pdfFileId: number | null = null; - if (created && typeof created === "object" && "id" in created) { - pdfFileId = Number(created.id); - } else if (typeof created === "number") { - pdfFileId = created; - } + const pdfFileId: string = `cloud:${cloudFile.id}`; return res.json({ success: true, @@ -942,6 +939,20 @@ router.get( const safeName = (f.filename || "claim.pdf").replace(/[/\\]/g, "_").trim() || "claim.pdf"; pdfEntries.push({ filename: safeName, data: buf }); } + + // New claim PDFs are saved to the patient's Cloud Storage "Claims" subfolder + const cloudFiles = await storage.listPatientCategoryCloudFiles(patientId, "Claims"); + for (const cf of cloudFiles) { + if (cf.id == null) continue; + const full = await storage.getFile(cf.id); + if (!full?.diskPath) continue; + const abs = path.join(process.cwd(), full.diskPath); + if (!fs.existsSync(abs)) continue; + const buf = fs.readFileSync(abs); + if (!buf.length) continue; + const safeName = (cf.name || "claim.pdf").replace(/[/\\]/g, "_").trim() || "claim.pdf"; + pdfEntries.push({ filename: safeName, data: buf }); + } } if (pdfEntries.length === 0) { diff --git a/apps/Backend/src/routes/documents.ts b/apps/Backend/src/routes/documents.ts index 27931146..5b816ce1 100755 --- a/apps/Backend/src/routes/documents.ts +++ b/apps/Backend/src/routes/documents.ts @@ -150,6 +150,34 @@ router.post( } ); +// Maps a legacy PdfGroup.titleKey to the matching Cloud Storage subfolder name +// so new (cloud-backed) PDFs show up alongside old (DB-blob) PDFs for the same category. +const TITLE_KEY_TO_CLOUD_CATEGORY: Record = { + ELIGIBILITY_STATUS: "Eligibility", + INSURANCE_CLAIM: "Claims", + INSURANCE_CLAIM_PREAUTH: "PreAuth", + CLAIM_STATUS: "Claim Status", + OTHER: "Attachments", +}; + +async function listCloudPdfsForGroup(groupId: number) { + const group = await storage.getPdfGroupById(groupId); + if (!group) return []; + const category = TITLE_KEY_TO_CLOUD_CATEGORY[group.titleKey as string]; + if (!category) return []; + + const cloudFiles = await storage.listPatientCategoryCloudFiles( + group.patientId, + category + ); + return cloudFiles.map((f) => ({ + id: `cloud:${f.id}`, + filename: f.name, + uploadedAt: f.createdAt, + mimeType: f.mimeType, + })); +} + router.get( "/pdf-files/group/:groupId", async (req: Request, res: Response): Promise => { @@ -160,7 +188,8 @@ router.get( } const groupId = parseInt(idParam); const files = await storage.getPdfFilesByGroupId(groupId); - res.json(files); + const cloudFiles = await listCloudPdfsForGroup(groupId); + res.json([...cloudFiles, ...(files as any[])]); } catch (err) { res.status(500).json({ error: "Internal server error" }); } @@ -203,6 +232,8 @@ router.get( // Decide whether client asked for paginated response const wantsPagination = typeof limit === "number"; + const cloudFiles = await listCloudPdfsForGroup(groupId); + if (wantsPagination) { // storage.getPdfFilesByGroupId with pagination should return { total, data } const result = await storage.getPdfFilesByGroupId(groupId, { @@ -214,14 +245,17 @@ router.get( // result should be { total, data }, but handle unexpected shapes defensively if (Array.isArray(result)) { // fallback: storage returned full array; compute total - return res.json({ total: result.length, data: result }); + const merged = [...cloudFiles, ...result]; + return res.json({ total: merged.length, data: merged }); } - return res.json(result); + const merged = [...cloudFiles, ...(result as any).data]; + return res.json({ total: (result as any).total + cloudFiles.length, data: merged }); } else { // no limit requested -> return all files for the group const all = (await storage.getPdfFilesByGroupId(groupId)) as PdfFile[]; - return res.json({ total: all.length, data: all }); + const merged = [...cloudFiles, ...(all as any[])]; + return res.json({ total: merged.length, data: merged }); } } catch (err) { console.error("GET /pdf-files/group/:groupId error:", err); @@ -238,6 +272,31 @@ router.get( if (!idParam) { return res.status(400).json({ error: "Missing ID" }); } + + // Cloud-Storage-backed PDF (new saves): id looks like "cloud:123" + const idStr = String(idParam); + if (idStr.startsWith("cloud:")) { + const cloudId = parseInt(idStr.slice("cloud:".length), 10); + if (Number.isNaN(cloudId)) { + return res.status(400).json({ error: "Invalid ID" }); + } + const cloudFile = await storage.getFile(cloudId); + if (!cloudFile) return res.status(404).json({ error: "PDF not found" }); + + res.setHeader("Content-Type", cloudFile.mimeType || "application/pdf"); + res.setHeader( + "Content-Disposition", + `attachment; filename="${cloudFile.name}"; filename*=UTF-8''${encodeURIComponent(cloudFile.name)}` + ); + try { + await storage.streamFileTo(res, cloudId); + } catch (streamErr) { + console.error("Error streaming cloud PDF file:", streamErr); + if (!res.headersSent) res.status(500).json({ error: "Failed to stream PDF" }); + } + return; + } + const id = parseInt(idParam, 10); if (Number.isNaN(id)) { return res.status(400).json({ error: "Invalid ID" }); @@ -409,7 +468,18 @@ router.delete( if (!idParam) { return res.status(400).json({ error: "Missing ID" }); } - const id = parseInt(idParam); + + const idStr = String(idParam); + if (idStr.startsWith("cloud:")) { + const cloudId = parseInt(idStr.slice("cloud:".length), 10); + if (Number.isNaN(cloudId)) { + return res.status(400).json({ error: "Invalid ID" }); + } + const success = await storage.deleteFile(cloudId); + return res.json({ success }); + } + + const id = parseInt(idStr); const success = await storage.deletePdfFile(id); res.json({ success }); diff --git a/apps/Backend/src/routes/insuranceStatus.ts b/apps/Backend/src/routes/insuranceStatus.ts index e3f4646f..2f516adf 100755 --- a/apps/Backend/src/routes/insuranceStatus.ts +++ b/apps/Backend/src/routes/insuranceStatus.ts @@ -521,36 +521,18 @@ router.post( ) { try { const pdfBuf = await fs.readFile(seleniumResult.pdf_path); - const groupTitle = "Eligibility Status"; - const groupTitleKey = "ELIGIBILITY_STATUS"; + const patientName = `${updatedPatient.firstName ?? ""} ${updatedPatient.lastName ?? ""}`.trim(); - let group = await storage.findPdfGroupByPatientTitleKey( + const cloudFile = await storage.savePdfToCloudStorage( + req.user.id, updatedPatient.id, - groupTitleKey - ); - if (!group) { - group = await storage.createPdfGroup( - updatedPatient.id, - groupTitle, - groupTitleKey - ); - } - if (!group?.id) - throw new Error("Failed to create/find pdf group"); - - const created = await storage.createPdfFile( - group.id, + patientName || `Patient ${updatedPatient.id}`, + "Eligibility", path.basename(seleniumResult.pdf_path), pdfBuf ); - if (created && typeof created === "object" && "id" in created) { - resultItem.pdfFileId = Number(created.id); - } else if (typeof created === "number") { - resultItem.pdfFileId = created; - } else if (created && (created as any).id) { - resultItem.pdfFileId = (created as any).id; - } + resultItem.pdfFileId = `cloud:${cloudFile.id}`; resultItem.processed = true; } catch (pdfErr: any) { diff --git a/apps/Backend/src/storage/cloudStorage-storage.ts b/apps/Backend/src/storage/cloudStorage-storage.ts index d58054b7..c82decef 100755 --- a/apps/Backend/src/storage/cloudStorage-storage.ts +++ b/apps/Backend/src/storage/cloudStorage-storage.ts @@ -143,6 +143,24 @@ export interface IStorage { patientId: number, patientName: string ): Promise; + getOrCreateSubfolder( + userId: number, + parentId: number, + name: string + ): Promise; + savePdfToCloudStorage( + userId: number, + patientId: number, + patientName: string, + category: "Eligibility" | "Claims" | "PreAuth" | "Claim Status" | "Attachments", + filename: string, + buffer: Buffer, + mimeType?: string + ): Promise; + listPatientCategoryCloudFiles( + patientId: number, + category: string + ): Promise; } /* ------------------------------- Implementation ------------------------------- */ @@ -546,6 +564,82 @@ export const cloudStorageStorage: IStorage = { return created as unknown as CloudFolder; }, + async getOrCreateSubfolder(userId: number, parentId: number, name: string) { + const existing = await db.cloudFolder.findFirst({ + where: { parentId, name }, + }); + if (existing) return existing as unknown as CloudFolder; + + try { + return await cloudStorageStorage.createFolder(userId, name, parentId); + } catch (err: any) { + // unique constraint on [userId, parentId, name] - another request created it first + if (err?.code === "P2002") { + const race = await db.cloudFolder.findFirst({ where: { parentId, name } }); + if (race) return race as unknown as CloudFolder; + } + throw err; + } + }, + + async savePdfToCloudStorage( + userId: number, + patientId: number, + patientName: string, + category: "Eligibility" | "Claims" | "PreAuth" | "Claim Status" | "Attachments", + filename: string, + buffer: Buffer, + mimeType: string = "application/pdf" + ) { + const patientFolder = await cloudStorageStorage.getOrCreatePatientFolder( + userId, + patientId, + patientName + ); + const subfolder = await cloudStorageStorage.getOrCreateSubfolder( + userId, + patientFolder.id!, + category + ); + const cloudFile = await cloudStorageStorage.initializeFileUpload( + userId, + filename, + mimeType, + BigInt(buffer.length), + 1, + subfolder.id! + ); + await cloudStorageStorage.appendFileChunk(cloudFile.id!, 0, buffer); + await cloudStorageStorage.finalizeFileUpload(cloudFile.id!); + const finalFile = await cloudStorageStorage.getFile(cloudFile.id!); + if (!finalFile) throw new Error("Failed to finalize PDF upload to cloud storage"); + return finalFile; + }, + + async listPatientCategoryCloudFiles(patientId: number, category: string) { + const patientFolder = await db.cloudFolder.findFirst({ where: { patientId } }); + if (!patientFolder) return []; + const subfolder = await db.cloudFolder.findFirst({ + where: { parentId: patientFolder.id, name: category }, + }); + if (!subfolder) return []; + const files = await db.cloudFile.findMany({ + where: { folderId: subfolder.id, isComplete: true }, + orderBy: { createdAt: "desc" }, + select: { + id: true, + name: true, + mimeType: true, + fileSize: true, + folderId: true, + isComplete: true, + createdAt: true, + updatedAt: true, + }, + }); + return files.map(serializeFile) as unknown as CloudFile[]; + }, + // --- STREAM --- async streamFileTo(resStream: NodeJS.WritableStream, fileId: number) { const file = await db.cloudFile.findUnique({ diff --git a/apps/Frontend/src/components/documents/file-preview-modal.tsx b/apps/Frontend/src/components/documents/file-preview-modal.tsx index 934aa2c5..be2ba23f 100755 --- a/apps/Frontend/src/components/documents/file-preview-modal.tsx +++ b/apps/Frontend/src/components/documents/file-preview-modal.tsx @@ -5,7 +5,7 @@ import { Maximize2, Minimize2, Download, X } from "lucide-react"; import { viewDocument } from "@/lib/api/documents"; type Props = { - fileId: number | null; + fileId: number | string | null; isOpen: boolean; onClose: () => void; initialFileName?: string | null; @@ -60,8 +60,8 @@ export default function DocumentsFilePreviewModal({ setLoading(false); return; } else if (isPatientDocument && fileId) { - // For patient documents, use the viewDocument API to get the URL - const documentUrl = viewDocument(fileId); + // For patient documents, use the viewDocument API to get the URL (always a numeric id) + const documentUrl = viewDocument(Number(fileId)); res = await fetch(documentUrl); } else { // For PDF files, use the existing endpoint diff --git a/apps/Frontend/src/components/insurance-status/bcbs-ma-button-modal.tsx b/apps/Frontend/src/components/insurance-status/bcbs-ma-button-modal.tsx index 5a362758..1171e36e 100644 --- a/apps/Frontend/src/components/insurance-status/bcbs-ma-button-modal.tsx +++ b/apps/Frontend/src/components/insurance-status/bcbs-ma-button-modal.tsx @@ -92,7 +92,7 @@ interface BcbsMaEligibilityButtonProps { isFormIncomplete: boolean; autoTrigger?: boolean; onAutoTriggered?: () => void; - onPdfReady: (pdfId: number, fallbackFilename: string | null) => void; + onPdfReady: (pdfId: number | string, fallbackFilename: string | null) => void; } export function BcbsMaEligibilityButton({ @@ -204,7 +204,7 @@ export function BcbsMaEligibilityButton({ queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE }); const pdfId = data.result?.pdfFileId; if (pdfId) { - onPdfReady(Number(pdfId), data.result?.pdfFilename ?? `eligibility_bcbs_ma_${memberId}.pdf`); + onPdfReady(pdfId, data.result?.pdfFilename ?? `eligibility_bcbs_ma_${memberId}.pdf`); } } else if (data.status === "failed") { const msg = data.error ?? "BCBS MA eligibility job failed."; diff --git a/apps/Frontend/src/components/insurance-status/cca-button-modal.tsx b/apps/Frontend/src/components/insurance-status/cca-button-modal.tsx index 3dc1e91d..2e0045c2 100644 --- a/apps/Frontend/src/components/insurance-status/cca-button-modal.tsx +++ b/apps/Frontend/src/components/insurance-status/cca-button-modal.tsx @@ -19,7 +19,7 @@ interface CCAEligibilityButtonProps { isFormIncomplete: boolean; autoTrigger?: boolean; onAutoTriggered?: () => void; - onPdfReady: (pdfId: number, fallbackFilename: string | null) => void; + onPdfReady: (pdfId: number | string, fallbackFilename: string | null) => void; } export function CCAEligibilityButton({ @@ -143,7 +143,7 @@ export function CCAEligibilityButton({ queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE }); const pdfId = data.result?.pdfFileId; if (pdfId) { - onPdfReady(Number(pdfId), data.result?.pdfFilename ?? `eligibility_cca_${memberId}.pdf`); + onPdfReady(pdfId, data.result?.pdfFilename ?? `eligibility_cca_${memberId}.pdf`); } } else if (data.status === "failed") { const msg = data.error ?? "CCA eligibility job failed."; diff --git a/apps/Frontend/src/components/insurance-status/ddma-buton-modal.tsx b/apps/Frontend/src/components/insurance-status/ddma-buton-modal.tsx index c6d4fc0f..f556e183 100755 --- a/apps/Frontend/src/components/insurance-status/ddma-buton-modal.tsx +++ b/apps/Frontend/src/components/insurance-status/ddma-buton-modal.tsx @@ -90,7 +90,7 @@ interface DdmaEligibilityButtonProps { isFormIncomplete: boolean; autoTrigger?: boolean; onAutoTriggered?: () => void; - onPdfReady: (pdfId: number, fallbackFilename: string | null) => void; + onPdfReady: (pdfId: number | string, fallbackFilename: string | null) => void; } export function DdmaEligibilityButton({ @@ -248,7 +248,7 @@ export function DdmaEligibilityButton({ if (pdfId) { const filename = data.result?.pdfFilename ?? `eligibility_ddma_${memberId}.pdf`; - onPdfReady(Number(pdfId), filename); + onPdfReady(pdfId, filename); } } else if (data.status === "failed") { const msg = data.error ?? "DDMA eligibility job failed."; @@ -326,7 +326,7 @@ export function DdmaEligibilityButton({ queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE }); const pdfId = data.result?.pdfFileId; if (pdfId) { - onPdfReady(Number(pdfId), data.result?.pdfFilename ?? `eligibility_ddma_${memberId}.pdf`); + onPdfReady(pdfId, data.result?.pdfFilename ?? `eligibility_ddma_${memberId}.pdf`); } } else if (data.status === "failed") { const msg = data.error ?? "DDMA eligibility job failed."; diff --git a/apps/Frontend/src/components/insurance-status/deltains-button-modal.tsx b/apps/Frontend/src/components/insurance-status/deltains-button-modal.tsx index ffa4c890..70020178 100644 --- a/apps/Frontend/src/components/insurance-status/deltains-button-modal.tsx +++ b/apps/Frontend/src/components/insurance-status/deltains-button-modal.tsx @@ -89,7 +89,7 @@ interface DeltaInsEligibilityButtonProps { isFormIncomplete: boolean; autoTrigger?: boolean; onAutoTriggered?: () => void; - onPdfReady: (pdfId: number, fallbackFilename: string | null) => void; + onPdfReady: (pdfId: number | string, fallbackFilename: string | null) => void; } export function DeltaInsEligibilityButton({ @@ -246,7 +246,7 @@ export function DeltaInsEligibilityButton({ queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE }); const pdfId = data.result?.pdfFileId; if (pdfId) { - onPdfReady(Number(pdfId), data.result?.pdfFilename ?? `eligibility_deltains_${memberId}.pdf`); + onPdfReady(pdfId, data.result?.pdfFilename ?? `eligibility_deltains_${memberId}.pdf`); } } else if (data.status === "failed") { const msg = data.error ?? "Delta Ins eligibility job failed."; diff --git a/apps/Frontend/src/components/insurance-status/dual-pdf-preview-modal.tsx b/apps/Frontend/src/components/insurance-status/dual-pdf-preview-modal.tsx index 3940d65d..81108626 100644 --- a/apps/Frontend/src/components/insurance-status/dual-pdf-preview-modal.tsx +++ b/apps/Frontend/src/components/insurance-status/dual-pdf-preview-modal.tsx @@ -3,7 +3,7 @@ import { Button } from "@/components/ui/button"; import { apiRequest } from "@/lib/queryClient"; export interface PdfPanelConfig { - pdfId?: number | null; + pdfId?: number | string | null; fallbackFilename?: string | null; label: string; autoDownload?: boolean; @@ -34,7 +34,7 @@ function parseFilename(header: string | null): string | null { return null; } -function usePdfBlob(open: boolean, pdfId?: number | null, fallbackFilename?: string | null) { +function usePdfBlob(open: boolean, pdfId?: number | string | null, fallbackFilename?: string | null) { const [blobUrl, setBlobUrl] = useState(null); const [filename, setFilename] = useState(null); const [loading, setLoading] = useState(false); diff --git a/apps/Frontend/src/components/insurance-status/pdf-preview-modal.tsx b/apps/Frontend/src/components/insurance-status/pdf-preview-modal.tsx index 5ea48968..6c7c5952 100755 --- a/apps/Frontend/src/components/insurance-status/pdf-preview-modal.tsx +++ b/apps/Frontend/src/components/insurance-status/pdf-preview-modal.tsx @@ -7,7 +7,7 @@ import { Maximize2, Minimize2 } from "lucide-react"; interface Props { open: boolean; onClose: () => void; - pdfId?: number | null; + pdfId?: number | string | null; fallbackFilename?: string | null; autoDownload?: boolean; } diff --git a/apps/Frontend/src/components/insurance-status/tufts-sco-button-modal.tsx b/apps/Frontend/src/components/insurance-status/tufts-sco-button-modal.tsx index 8239afeb..e2417069 100644 --- a/apps/Frontend/src/components/insurance-status/tufts-sco-button-modal.tsx +++ b/apps/Frontend/src/components/insurance-status/tufts-sco-button-modal.tsx @@ -89,7 +89,7 @@ interface TuftsSCOEligibilityButtonProps { isFormIncomplete: boolean; autoTrigger?: boolean; onAutoTriggered?: () => void; - onPdfReady: (pdfId: number, fallbackFilename: string | null) => void; + onPdfReady: (pdfId: number | string, fallbackFilename: string | null) => void; } export function TuftsSCOEligibilityButton({ @@ -243,7 +243,7 @@ export function TuftsSCOEligibilityButton({ queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE }); const pdfId = data.result?.pdfFileId; if (pdfId) { - onPdfReady(Number(pdfId), data.result?.pdfFilename ?? `eligibility_unitedsco_${memberId}.pdf`); + onPdfReady(pdfId, data.result?.pdfFilename ?? `eligibility_unitedsco_${memberId}.pdf`); } } else if (data.status === "failed") { const msg = data.error ?? "Tufts SCO eligibility job failed."; diff --git a/apps/Frontend/src/components/insurance-status/united-sco-button-modal.tsx b/apps/Frontend/src/components/insurance-status/united-sco-button-modal.tsx index b47f0a32..110cd8af 100644 --- a/apps/Frontend/src/components/insurance-status/united-sco-button-modal.tsx +++ b/apps/Frontend/src/components/insurance-status/united-sco-button-modal.tsx @@ -89,7 +89,7 @@ interface UnitedSCOEligibilityButtonProps { isFormIncomplete: boolean; autoTrigger?: boolean; onAutoTriggered?: () => void; - onPdfReady: (pdfId: number, fallbackFilename: string | null) => void; + onPdfReady: (pdfId: number | string, fallbackFilename: string | null) => void; } export function UnitedSCOEligibilityButton({ @@ -243,7 +243,7 @@ export function UnitedSCOEligibilityButton({ queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE }); const pdfId = data.result?.pdfFileId; if (pdfId) { - onPdfReady(Number(pdfId), data.result?.pdfFilename ?? `eligibility_unitedsco_${memberId}.pdf`); + onPdfReady(pdfId, data.result?.pdfFilename ?? `eligibility_unitedsco_${memberId}.pdf`); } } else if (data.status === "failed") { const msg = data.error ?? "United SCO eligibility job failed."; diff --git a/apps/Frontend/src/pages/claims-page.tsx b/apps/Frontend/src/pages/claims-page.tsx index 5528d15e..bcb6976f 100755 --- a/apps/Frontend/src/pages/claims-page.tsx +++ b/apps/Frontend/src/pages/claims-page.tsx @@ -46,7 +46,7 @@ export default function ClaimsPage() { >(null); // PDF preview modal state const [previewOpen, setPreviewOpen] = useState(false); - const [previewPdfId, setPreviewPdfId] = useState(null); + const [previewPdfId, setPreviewPdfId] = useState(null); const [previewFallbackFilename, setPreviewFallbackFilename] = useState(null); const dispatch = useAppDispatch(); const { status, message, show } = useAppSelector( diff --git a/apps/Frontend/src/pages/documents-page.tsx b/apps/Frontend/src/pages/documents-page.tsx index de3a0f7b..3cac2d1c 100755 --- a/apps/Frontend/src/pages/documents-page.tsx +++ b/apps/Frontend/src/pages/documents-page.tsx @@ -15,7 +15,13 @@ import { Eye, Trash, Download, FolderOpen, FileText, HardDrive } from "lucide-re import { useAuth } from "@/hooks/use-auth"; import { DeleteConfirmationDialog } from "@/components/ui/deleteDialog"; import { PatientTable } from "@/components/patients/patient-table"; -import { Patient, PdfFile } from "@repo/db/types"; +import { Patient, PdfFile as PrismaPdfFile } from "@repo/db/types"; + +// A PDF list item as returned by /api/documents/pdf-files/group/:groupId and +// /api/documents/recent-pdf-files/group/:groupId — id is a plain number for +// legacy (Postgres-blob) records, or a "cloud:" string for records saved +// to the new Cloud Storage backend. +type PdfFile = Omit & { id: number | string }; import { Pagination, PaginationContent, @@ -50,7 +56,7 @@ export default function DocumentsPage() { const [showPatientDocuments, setShowPatientDocuments] = useState(false); // Document preview state - const [previewDocumentId, setPreviewDocumentId] = useState(null); + const [previewDocumentId, setPreviewDocumentId] = useState(null); const [isPreviewModalOpen, setIsPreviewModalOpen] = useState(false); // Delete document state @@ -245,7 +251,7 @@ export default function DocumentsPage() { // DELETE mutation const deletePdfMutation = useMutation({ - mutationFn: async (id: number) => { + mutationFn: async (id: number | string) => { await apiRequest("DELETE", `/api/documents/pdf-files/${id}`); }, onSuccess: () => { @@ -267,7 +273,7 @@ export default function DocumentsPage() { const handleConfirmDeletePdf = () => { if (currentPdf) { - deletePdfMutation.mutate(Number(currentPdf.id)); + deletePdfMutation.mutate(currentPdf.id); } else { toast({ title: "Error", @@ -277,12 +283,12 @@ export default function DocumentsPage() { } }; - const handleViewPdf = (pdfId: number, filename?: string) => { + const handleViewPdf = (pdfId: number | string, filename?: string) => { setPreviewDocumentId(pdfId); setIsPreviewModalOpen(true); }; - const handleDownloadPdf = async (pdfId: number, filename: string) => { + const handleDownloadPdf = async (pdfId: number | string, filename: string) => { const res = await apiRequest("GET", `/api/documents/pdf-files/${pdfId}`); const arrayBuffer = await res.arrayBuffer(); const blob = new Blob([arrayBuffer], { type: "application/pdf" }); @@ -447,7 +453,7 @@ export default function DocumentsPage() { size="sm" onClick={() => handleViewPdf( - Number(pdf.id), + pdf.id, pdf.filename ) } @@ -459,7 +465,7 @@ export default function DocumentsPage() { size="sm" onClick={() => handleDownloadPdf( - Number(pdf.id), + pdf.id, pdf.filename ) } diff --git a/apps/Frontend/src/pages/insurance-status-page.tsx b/apps/Frontend/src/pages/insurance-status-page.tsx index 3f23137b..010b42e7 100755 --- a/apps/Frontend/src/pages/insurance-status-page.tsx +++ b/apps/Frontend/src/pages/insurance-status-page.tsx @@ -142,14 +142,14 @@ export default function InsuranceStatusPage() { // PDF preview modal state const [previewOpen, setPreviewOpen] = useState(false); - const [previewPdfId, setPreviewPdfId] = useState(null); + const [previewPdfId, setPreviewPdfId] = useState(null); const [previewFallbackFilename, setPreviewFallbackFilename] = useState< string | null >(null); // Dual PDF modal state (used by MH Eligibility & History) const [dualPreviewOpen, setDualPreviewOpen] = useState(false); - const [dualEligibilityPdfId, setDualEligibilityPdfId] = useState(null); + const [dualEligibilityPdfId, setDualEligibilityPdfId] = useState(null); const [dualEligibilityFilename, setDualEligibilityFilename] = useState(null); const [dualMemberDetailsPdfId, setDualMemberDetailsPdfId] = useState(null); const [dualMemberDetailsFilename, setDualMemberDetailsFilename] = useState(null); @@ -158,7 +158,7 @@ export default function InsuranceStatusPage() { // CMSP PDF modal state (used by CMSP Eligibility & History & Remaining) const [cmspPreviewOpen, setCmspPreviewOpen] = useState(false); - const [cmspEligibilityPdfId, setCmspEligibilityPdfId] = useState(null); + const [cmspEligibilityPdfId, setCmspEligibilityPdfId] = useState(null); const [cmspEligibilityFilename, setCmspEligibilityFilename] = useState(null); const [cmspMemberDetailsPdfId, setCmspMemberDetailsPdfId] = useState(null); const [cmspMemberDetailsFilename, setCmspMemberDetailsFilename] = useState(null); @@ -381,7 +381,7 @@ export default function InsuranceStatusPage() { void tryAppointmentFromChatbot(); if (jobResult.pdfFileId) { - setPreviewPdfId(Number(jobResult.pdfFileId)); + setPreviewPdfId(jobResult.pdfFileId); setPreviewFallbackFilename(jobResult.pdfFilename ?? `eligibility_${memberId}.pdf`); setPreviewOpen(true); } @@ -466,7 +466,7 @@ export default function InsuranceStatusPage() { setSelectedPatient(null); if (jobResult.pdfFileId) { - setPreviewPdfId(Number(jobResult.pdfFileId)); + setPreviewPdfId(jobResult.pdfFileId); setPreviewFallbackFilename(jobResult.pdfFilename ?? `eligibility_${memberId}.pdf`); setPreviewOpen(true); } @@ -579,7 +579,7 @@ export default function InsuranceStatusPage() { // Open all PDFs side by side in the modal if (jobResult.pdfFileId || jobResult.memberDetailsPdfFileId || jobResult.historyPdfFileId) { - setDualEligibilityPdfId(jobResult.pdfFileId ? Number(jobResult.pdfFileId) : null); + setDualEligibilityPdfId(jobResult.pdfFileId ?? null); setDualEligibilityFilename(jobResult.pdfFilename ?? `eligibility_${memberId}.pdf`); setDualMemberDetailsPdfId(jobResult.memberDetailsPdfFileId ? Number(jobResult.memberDetailsPdfFileId) : null); setDualMemberDetailsFilename(jobResult.memberDetailsPdfFilename ?? `eligibility_member_details_${memberId}.pdf`); @@ -649,7 +649,7 @@ export default function InsuranceStatusPage() { // Open 4-panel modal if (jobResult.pdfFileId || jobResult.memberDetailsPdfFileId || jobResult.historyPdfFileId || jobResult.accumulatorPdfFileId) { - setCmspEligibilityPdfId(jobResult.pdfFileId ? Number(jobResult.pdfFileId) : null); + setCmspEligibilityPdfId(jobResult.pdfFileId ?? null); setCmspEligibilityFilename(jobResult.pdfFilename ?? `cmsp_eligibility_${memberId}.pdf`); setCmspMemberDetailsPdfId(jobResult.memberDetailsPdfFileId ? Number(jobResult.memberDetailsPdfFileId) : null); setCmspMemberDetailsFilename(jobResult.memberDetailsPdfFilename ?? `cmsp_member_details_${memberId}.pdf`);