feat(pdf-preview-modal) - updated files1

This commit is contained in:
2025-09-21 01:45:53 +05:30
parent b7d2289576
commit 536c2fe3ac
3 changed files with 246 additions and 2 deletions

View File

@@ -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({

View File

@@ -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>
);
}

View File

@@ -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>
); );
} }