feat(pdf-preview-modal) - updated files1
This commit is contained in:
@@ -46,6 +46,8 @@ router.post(
|
|||||||
const result =
|
const result =
|
||||||
await forwardToSeleniumInsuranceEligibilityAgent(enrichedData);
|
await forwardToSeleniumInsuranceEligibilityAgent(enrichedData);
|
||||||
|
|
||||||
|
let createdPdfFileId: number | null = null;
|
||||||
|
|
||||||
// ✅ Step 1: Check result and update patient status
|
// ✅ Step 1: Check result and update patient status
|
||||||
const patient = await storage.getPatientByInsuranceId(
|
const patient = await storage.getPatientByInsuranceId(
|
||||||
insuranceEligibilityData.memberId
|
insuranceEligibilityData.memberId
|
||||||
@@ -80,12 +82,18 @@ router.post(
|
|||||||
if (!group?.id) {
|
if (!group?.id) {
|
||||||
throw new Error("PDF group creation failed: missing group ID");
|
throw new Error("PDF group creation failed: missing group ID");
|
||||||
}
|
}
|
||||||
await storage.createPdfFile(
|
|
||||||
|
const created = await storage.createPdfFile(
|
||||||
group.id,
|
group.id,
|
||||||
path.basename(result.pdf_path),
|
path.basename(result.pdf_path),
|
||||||
pdfBuffer
|
pdfBuffer
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// created could be { id, filename } or just id, adapt to your storage API.
|
||||||
|
if (created && typeof created === "object" && "id" in created) {
|
||||||
|
createdPdfFileId = Number(created.id);
|
||||||
|
}
|
||||||
|
|
||||||
await fs.unlink(result.pdf_path);
|
await fs.unlink(result.pdf_path);
|
||||||
|
|
||||||
result.pdfUploadStatus = `PDF saved to group: ${group.title}`;
|
result.pdfUploadStatus = `PDF saved to group: ${group.title}`;
|
||||||
@@ -101,6 +109,7 @@ router.post(
|
|||||||
res.json({
|
res.json({
|
||||||
patientUpdateStatus: result.patientUpdateStatus,
|
patientUpdateStatus: result.patientUpdateStatus,
|
||||||
pdfUploadStatus: result.pdfUploadStatus,
|
pdfUploadStatus: result.pdfUploadStatus,
|
||||||
|
pdfFileId: createdPdfFileId,
|
||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -175,6 +184,8 @@ router.post(
|
|||||||
const result =
|
const result =
|
||||||
await forwardToSeleniumInsuranceClaimStatusAgent(enrichedData);
|
await forwardToSeleniumInsuranceClaimStatusAgent(enrichedData);
|
||||||
|
|
||||||
|
let createdPdfFileId: number | null = null;
|
||||||
|
|
||||||
// ✅ Step 1: Check result
|
// ✅ Step 1: Check result
|
||||||
const patient = await storage.getPatientByInsuranceId(
|
const patient = await storage.getPatientByInsuranceId(
|
||||||
insuranceClaimStatusData.memberId
|
insuranceClaimStatusData.memberId
|
||||||
@@ -239,7 +250,15 @@ router.post(
|
|||||||
|
|
||||||
// Use the basename for storage
|
// Use the basename for storage
|
||||||
const basename = path.basename(generatedPdfPath);
|
const basename = path.basename(generatedPdfPath);
|
||||||
await storage.createPdfFile(group.id, basename, pdfBuffer);
|
const created = await storage.createPdfFile(
|
||||||
|
group.id,
|
||||||
|
basename,
|
||||||
|
pdfBuffer
|
||||||
|
);
|
||||||
|
|
||||||
|
if (created && typeof created === "object" && "id" in created) {
|
||||||
|
createdPdfFileId = Number(created.id);
|
||||||
|
}
|
||||||
|
|
||||||
// Clean up temp files:
|
// Clean up temp files:
|
||||||
try {
|
try {
|
||||||
@@ -268,7 +287,9 @@ router.post(
|
|||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
pdfUploadStatus: result.pdfUploadStatus,
|
pdfUploadStatus: result.pdfUploadStatus,
|
||||||
|
pdfFileId: createdPdfFileId,
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
|
|||||||
@@ -0,0 +1,183 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { apiRequest } from "@/lib/queryClient";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
pdfId?: number | null;
|
||||||
|
fallbackFilename?: string | null; // optional fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse filename from Content-Disposition header.
|
||||||
|
* Returns string | null.
|
||||||
|
*/
|
||||||
|
function parseFilenameFromContentDisposition(header: string | null): string | null {
|
||||||
|
if (!header) return null;
|
||||||
|
|
||||||
|
// filename* (RFC 5987): filename*=UTF-8''encoded
|
||||||
|
const filenameStarMatch = header.match(/filename\*\s*=\s*([^;]+)/i);
|
||||||
|
if (filenameStarMatch && filenameStarMatch[1]) {
|
||||||
|
let raw = filenameStarMatch[1].trim();
|
||||||
|
raw = raw.replace(/^"(.*)"$/, "$1"); // remove quotes
|
||||||
|
const parts = raw.split("''");
|
||||||
|
if (parts.length === 2 && parts[1]) {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(parts[1]);
|
||||||
|
} catch {
|
||||||
|
return parts[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(raw);
|
||||||
|
} catch {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// filename="..." or filename=...
|
||||||
|
const filenameMatchQuoted = header.match(/filename\s*=\s*"([^"]+)"/i);
|
||||||
|
if (filenameMatchQuoted && filenameMatchQuoted[1]) {
|
||||||
|
return filenameMatchQuoted[1].trim();
|
||||||
|
}
|
||||||
|
const filenameMatch = header.match(/filename\s*=\s*([^;]+)/i);
|
||||||
|
if (filenameMatch && filenameMatch[1]) {
|
||||||
|
return filenameMatch[1].trim().replace(/^"(.*)"$/, "$1");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PdfPreviewModal({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
pdfId,
|
||||||
|
fallbackFilename = null,
|
||||||
|
}: Props) {
|
||||||
|
const [fileBlobUrl, setFileBlobUrl] = useState<string | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [resolvedFilename, setResolvedFilename] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
|
||||||
|
let objectUrl: string | null = null;
|
||||||
|
const controller = new AbortController();
|
||||||
|
let aborted = false;
|
||||||
|
|
||||||
|
const fetchPdf = async () => {
|
||||||
|
if (!pdfId) {
|
||||||
|
setError("No PDF id provided.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setResolvedFilename(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use the same apiRequest signature as DocumentsPage.
|
||||||
|
// We don't pass the AbortSignal into apiRequest to avoid signature mismatch.
|
||||||
|
const res = await apiRequest("GET", `/api/documents/pdf-files/${pdfId}`);
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
throw new Error("No response from server");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
// try to get error text for better message
|
||||||
|
const txt = await res.text().catch(() => "");
|
||||||
|
throw new Error(txt || `Failed to fetch PDF: ${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// read headers safely
|
||||||
|
const contentDispHeader =
|
||||||
|
res.headers?.get?.("content-disposition") ??
|
||||||
|
res.headers?.get?.("Content-Disposition") ??
|
||||||
|
null;
|
||||||
|
|
||||||
|
const parsedFilename = parseFilenameFromContentDisposition(contentDispHeader);
|
||||||
|
// final name (string)
|
||||||
|
const finalName = parsedFilename ?? fallbackFilename ?? `file_${pdfId}.pdf`;
|
||||||
|
setResolvedFilename(finalName);
|
||||||
|
|
||||||
|
const arrayBuffer = await res.arrayBuffer();
|
||||||
|
if (aborted) return;
|
||||||
|
|
||||||
|
const blob = new Blob([arrayBuffer], { type: "application/pdf" });
|
||||||
|
objectUrl = URL.createObjectURL(blob);
|
||||||
|
setFileBlobUrl(objectUrl);
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err && (err.name === "AbortError" || err.message === "The user aborted a request.")) {
|
||||||
|
// ignore abort error
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.error("PdfPreviewModal fetch error:", err);
|
||||||
|
setError(err?.message ?? "Failed to fetch PDF");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchPdf();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
aborted = true;
|
||||||
|
controller.abort();
|
||||||
|
if (objectUrl) {
|
||||||
|
URL.revokeObjectURL(objectUrl);
|
||||||
|
}
|
||||||
|
setFileBlobUrl(null);
|
||||||
|
setError(null);
|
||||||
|
setLoading(false);
|
||||||
|
setResolvedFilename(null);
|
||||||
|
};
|
||||||
|
}, [open, pdfId, fallbackFilename]);
|
||||||
|
|
||||||
|
if (!open) return null;
|
||||||
|
|
||||||
|
const handleDownload = () => {
|
||||||
|
if (!fileBlobUrl) return;
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = fileBlobUrl;
|
||||||
|
a.download = resolvedFilename ?? `file_${pdfId ?? "unknown"}.pdf`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||||
|
<div className="bg-white rounded-lg shadow-lg w-11/12 md:w-3/4 lg:w-2/3 h-4/5 flex flex-col">
|
||||||
|
<div className="flex items-center justify-between p-4 border-b">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold">{resolvedFilename ?? "PDF Preview"}</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">{pdfId ? `ID: ${pdfId}` : ""}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button variant="ghost" onClick={onClose}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" onClick={handleDownload} disabled={!fileBlobUrl || loading}>
|
||||||
|
Download
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-auto p-4">
|
||||||
|
{loading && <div>Loading PDF…</div>}
|
||||||
|
{error && <div className="text-destructive">Error: {error}</div>}
|
||||||
|
{fileBlobUrl && (
|
||||||
|
<iframe
|
||||||
|
title="PDF Preview"
|
||||||
|
src={fileBlobUrl}
|
||||||
|
className="w-full h-full border"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ import { formatLocalDate, parseLocalDate } from "@/utils/dateUtils";
|
|||||||
import { InsertPatient, Patient } from "@repo/db/types";
|
import { InsertPatient, Patient } from "@repo/db/types";
|
||||||
import { DateInput } from "@/components/ui/dateInput";
|
import { DateInput } from "@/components/ui/dateInput";
|
||||||
import { QK_PATIENTS_BASE } from "@/components/patients/patient-table";
|
import { QK_PATIENTS_BASE } from "@/components/patients/patient-table";
|
||||||
|
import { PdfPreviewModal } from "@/components/insurance-status/pdf-preview-modal";
|
||||||
|
|
||||||
export default function EligibilityClaimStatusPage() {
|
export default function EligibilityClaimStatusPage() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@@ -44,6 +45,13 @@ export default function EligibilityClaimStatusPage() {
|
|||||||
useState(false);
|
useState(false);
|
||||||
const [isCheckingClaimStatus, setIsCheckingClaimStatus] = useState(false);
|
const [isCheckingClaimStatus, setIsCheckingClaimStatus] = useState(false);
|
||||||
|
|
||||||
|
// PDF preview modal state
|
||||||
|
const [previewOpen, setPreviewOpen] = useState(false);
|
||||||
|
const [previewPdfId, setPreviewPdfId] = useState<number | null>(null);
|
||||||
|
const [previewFallbackFilename, setPreviewFallbackFilename] = useState<
|
||||||
|
string | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
// Populate fields from selected patient
|
// Populate fields from selected patient
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedPatient) {
|
if (selectedPatient) {
|
||||||
@@ -135,6 +143,16 @@ export default function EligibilityClaimStatusPage() {
|
|||||||
"Your Patient Eligibility is fetched and updated, Kindly search through the patient.",
|
"Your Patient Eligibility is fetched and updated, Kindly search through the patient.",
|
||||||
variant: "default",
|
variant: "default",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If server returned pdfFileId: open preview modal
|
||||||
|
if (result.pdfFileId) {
|
||||||
|
setPreviewPdfId(Number(result.pdfFileId));
|
||||||
|
// optional fallback name while header is parsed
|
||||||
|
setPreviewFallbackFilename(
|
||||||
|
result.pdfFilename ?? `eligibility_${memberId}.pdf`
|
||||||
|
);
|
||||||
|
setPreviewOpen(true);
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
dispatch(
|
dispatch(
|
||||||
setTaskStatus({
|
setTaskStatus({
|
||||||
@@ -188,6 +206,16 @@ export default function EligibilityClaimStatusPage() {
|
|||||||
"Your Claim Status is fetched and updated, Kindly search through the patient.",
|
"Your Claim Status is fetched and updated, Kindly search through the patient.",
|
||||||
variant: "default",
|
variant: "default",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If server returned pdfFileId: open preview modal
|
||||||
|
if (result.pdfFileId) {
|
||||||
|
setPreviewPdfId(Number(result.pdfFileId));
|
||||||
|
// optional fallback name while header is parsed
|
||||||
|
setPreviewFallbackFilename(
|
||||||
|
result.pdfFilename ?? `eligibility_${memberId}.pdf`
|
||||||
|
);
|
||||||
|
setPreviewOpen(true);
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
dispatch(
|
dispatch(
|
||||||
setTaskStatus({
|
setTaskStatus({
|
||||||
@@ -401,6 +429,18 @@ export default function EligibilityClaimStatusPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Pdf preview modal */}
|
||||||
|
<PdfPreviewModal
|
||||||
|
open={previewOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setPreviewOpen(false);
|
||||||
|
setPreviewPdfId(null);
|
||||||
|
setPreviewFallbackFilename(null);
|
||||||
|
}}
|
||||||
|
pdfId={previewPdfId ?? undefined}
|
||||||
|
fallbackFilename={previewFallbackFilename ?? undefined} // optional
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user