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:<id>" ids. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.";
|
||||
|
||||
@@ -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.";
|
||||
|
||||
@@ -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.";
|
||||
|
||||
@@ -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.";
|
||||
|
||||
@@ -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<string | null>(null);
|
||||
const [filename, setFilename] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.";
|
||||
|
||||
@@ -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.";
|
||||
|
||||
@@ -46,7 +46,7 @@ export default function ClaimsPage() {
|
||||
>(null);
|
||||
// PDF preview modal state
|
||||
const [previewOpen, setPreviewOpen] = useState(false);
|
||||
const [previewPdfId, setPreviewPdfId] = useState<number | null>(null);
|
||||
const [previewPdfId, setPreviewPdfId] = useState<number | string | null>(null);
|
||||
const [previewFallbackFilename, setPreviewFallbackFilename] = useState<string | null>(null);
|
||||
const dispatch = useAppDispatch();
|
||||
const { status, message, show } = useAppSelector(
|
||||
|
||||
@@ -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:<id>" string for records saved
|
||||
// to the new Cloud Storage backend.
|
||||
type PdfFile = Omit<PrismaPdfFile, "id"> & { 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<number | null>(null);
|
||||
const [previewDocumentId, setPreviewDocumentId] = useState<number | string | null>(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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -142,14 +142,14 @@ export default function InsuranceStatusPage() {
|
||||
|
||||
// PDF preview modal state
|
||||
const [previewOpen, setPreviewOpen] = useState(false);
|
||||
const [previewPdfId, setPreviewPdfId] = useState<number | null>(null);
|
||||
const [previewPdfId, setPreviewPdfId] = useState<number | string | null>(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<number | null>(null);
|
||||
const [dualEligibilityPdfId, setDualEligibilityPdfId] = useState<number | string | null>(null);
|
||||
const [dualEligibilityFilename, setDualEligibilityFilename] = useState<string | null>(null);
|
||||
const [dualMemberDetailsPdfId, setDualMemberDetailsPdfId] = useState<number | null>(null);
|
||||
const [dualMemberDetailsFilename, setDualMemberDetailsFilename] = useState<string | null>(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<number | null>(null);
|
||||
const [cmspEligibilityPdfId, setCmspEligibilityPdfId] = useState<number | string | null>(null);
|
||||
const [cmspEligibilityFilename, setCmspEligibilityFilename] = useState<string | null>(null);
|
||||
const [cmspMemberDetailsPdfId, setCmspMemberDetailsPdfId] = useState<number | null>(null);
|
||||
const [cmspMemberDetailsFilename, setCmspMemberDetailsFilename] = useState<string | null>(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`);
|
||||
|
||||
Reference in New Issue
Block a user