From 56ef5ab65d2cbd1ead067f619bc331a0f02b62c1 Mon Sep 17 00:00:00 2001 From: Vishnu Date: Fri, 13 Jun 2025 22:59:53 +0530 Subject: [PATCH] pdf upload checkpoint1 --- README.md | 5 ++ apps/Backend/src/routes/claim-pdf.ts | 67 ++++++++++++++++++ apps/Backend/src/routes/claims.ts | 26 ++++--- apps/Backend/src/storage/index.ts | 94 ++++++++++++++++++++++++- apps/Frontend/src/pages/claims-page.tsx | 11 ++- package.json | 2 +- packages/db/prisma/schema.prisma | 19 ++++- packages/db/scripts/patch-zod-buffer.ts | 15 ++++ packages/db/usedSchemas/index.ts | 3 +- 9 files changed, 226 insertions(+), 16 deletions(-) create mode 100644 apps/Backend/src/routes/claim-pdf.ts create mode 100644 packages/db/scripts/patch-zod-buffer.ts diff --git a/README.md b/README.md index 6dc435a..34a3a52 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,11 @@ npm run db:generate npm run db:migrate ``` +- Generate the db types: +```sh +npm run db:generate +``` + - seed the db if: ```sh npm run db:seed diff --git a/apps/Backend/src/routes/claim-pdf.ts b/apps/Backend/src/routes/claim-pdf.ts new file mode 100644 index 0000000..5b4147d --- /dev/null +++ b/apps/Backend/src/routes/claim-pdf.ts @@ -0,0 +1,67 @@ +import { Router } from "express"; +import { Request, Response } from "express"; +import { storage } from "../storage"; +import { z } from "zod"; +import { ClaimUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas"; + +const router = Router(); + + +router.post("/claim-pdf/upload", upload.single("file"), async (req: Request, res: Response) => { + const { patientId, claimId } = req.body; + const file = req.file; + + if (!file || !patientId) return res.status(400).json({ error: "Missing file or patientId" }); + + const created = await storage.createClaimPdf({ + filename: file.originalname, + patientId: parseInt(patientId), + claimId: claimId ? parseInt(claimId) : undefined, + pdfData: file.buffer, + }); + + res.json(created); +}); + +router.get("/claim-pdf/recent", async (req: Request, res: Response) => { + const limit = parseInt(req.query.limit as string) || 5; + const offset = parseInt(req.query.offset as string) || 0; + + const recent = await storage.getRecentClaimPdfs(limit, offset); + res.json(recent); +}); + +router.get("/claim-pdf/:id", async (req: Request, res: Response) => { + const id = parseInt(req.params.id); + const pdf = await storage.getClaimPdfById(id); + + if (!pdf) return res.status(404).json({ error: "PDF not found" }); + + res.setHeader("Content-Type", "application/pdf"); + res.send(pdf.pdfData); +}); + +router.delete("/claim-pdf/:id", async (req: Request, res: Response) => { + const id = parseInt(req.params.id); + const success = await storage.deleteClaimPdf(id); + + res.json({ success }); +}); + +router.put("/claim-pdf/:id", upload.single("file"), async (req: Request, res: Response) => { + const id = parseInt(req.params.id); + const file = req.file; + const claimId = req.body.claimId ? parseInt(req.body.claimId) : undefined; + + const updated = await storage.updateClaimPdf(id, { + claimId, + filename: file?.originalname, + pdfData: file?.buffer, + }); + + if (!updated) return res.status(404).json({ error: "PDF not found or update failed" }); + + res.json(updated); +}); + +export default router; diff --git a/apps/Backend/src/routes/claims.ts b/apps/Backend/src/routes/claims.ts index 85dfaac..6b377ea 100644 --- a/apps/Backend/src/routes/claims.ts +++ b/apps/Backend/src/routes/claims.ts @@ -6,7 +6,6 @@ import { ClaimUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas"; import multer from "multer"; import { forwardToSeleniumAgent, forwardToSeleniumAgent2 } from "../services/seleniumClient"; import path from "path"; -import fs from "fs"; import axios from "axios"; const router = Router(); @@ -96,6 +95,14 @@ router.post( } try{ + const { patientId, claimId } = req.body; // ✅ Extract patientId from the body + + if (!patientId || !claimId) { + return res.status(400).json({ error: "Missing patientId or claimId" }); + } + const parsedPatientId = parseInt(patientId); + const parsedClaimId = parseInt(claimId); + const result = await forwardToSeleniumAgent2(); if (result.status !== "success") { @@ -104,17 +111,14 @@ router.post( const pdfUrl = result.pdf_url; const filename = path.basename(new URL(pdfUrl).pathname); - - const tempDir = path.join(__dirname, "..", "..", "temp"); - if (!fs.existsSync(tempDir)) { - fs.mkdirSync(tempDir, { recursive: true }); - } - - const filePath = path.join(tempDir, filename); - - // Download the PDF directly using axios const pdfResponse = await axios.get(pdfUrl, { responseType: "arraybuffer" }); - fs.writeFileSync(filePath, pdfResponse.data); + + await storage.createClaimPdf( + parsedPatientId, + parsedClaimId, + filename, + pdfResponse.data + ); return res.json({ success: true, diff --git a/apps/Backend/src/storage/index.ts b/apps/Backend/src/storage/index.ts index 031b15b..c762ef9 100644 --- a/apps/Backend/src/storage/index.ts +++ b/apps/Backend/src/storage/index.ts @@ -6,6 +6,7 @@ import { StaffUncheckedCreateInputObjectSchema, ClaimUncheckedCreateInputObjectSchema, InsuranceCredentialUncheckedCreateInputObjectSchema, + ClaimPdfUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas"; import { z } from "zod"; @@ -145,6 +146,15 @@ type ClaimWithServiceLines = Claim & { }[]; }; +// claim types: +type ClaimPdf = z.infer; + +export interface ClaimPdfMetadata { + id: number; + filename: string; + uploadedAt: Date; +} + export interface IStorage { // User methods getUser(id: number): Promise; @@ -211,6 +221,27 @@ export interface IStorage { userId: number, siteKey: string ): Promise; + + // Claim PDF Methods + createClaimPdf( + patientId: number, + claimId: number, + filename: string, + pdfData: Buffer + ): Promise; + + getClaimPdfById(id: number): Promise; + + getAllClaimPdfs(): Promise; + + getRecentClaimPdfs(limit: number, offset: number): Promise; + + deleteClaimPdf(id: number): Promise; + + updateClaimPdf( + id: number, + updates: Partial> + ): Promise; } export const storage: IStorage = { @@ -474,5 +505,66 @@ export const storage: IStorage = { }); }, - + // pdf claims + async createClaimPdf( + patientId, + claimId, + filename, + pdfData + ): Promise { + return db.claimPdf.create({ + data: { + patientId, + claimId, + filename, + pdfData, + }, + }); + }, + + async getClaimPdfById(id: number): Promise { + const pdf = await db.claimPdf.findUnique({ where: { id } }); + return pdf ?? undefined; + }, + + async getAllClaimPdfs(): Promise { + return db.claimPdf.findMany({ + select: { id: true, filename: true, uploadedAt: true }, + orderBy: { uploadedAt: "desc" }, + }); + }, + + async getRecentClaimPdfs(limit: number, offset: number): Promise { + return db.claimPdf.findMany({ + skip: offset, + take: limit, + orderBy: { uploadedAt: "desc" }, + select: { + id: true, + filename: true, + uploadedAt: true, + }, + }); + }, + + async deleteClaimPdf(id: number): Promise { + try { + await db.claimPdf.delete({ where: { id } }); + return true; + } catch { + return false; + } + }, + + async updateClaimPdf( + id: number, + updates: Partial> + ): Promise { + try { + const updated = await db.claimPdf.update({ where: { id }, data: updates }); + return updated; + } catch { + return undefined; + } + }, }; diff --git a/apps/Frontend/src/pages/claims-page.tsx b/apps/Frontend/src/pages/claims-page.tsx index 53f152f..83790ec 100644 --- a/apps/Frontend/src/pages/claims-page.tsx +++ b/apps/Frontend/src/pages/claims-page.tsx @@ -9,6 +9,7 @@ import { useAuth } from "@/hooks/use-auth"; import { PatientUncheckedCreateInputObjectSchema, AppointmentUncheckedCreateInputObjectSchema, + ClaimUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas"; import { AlertCircle, CheckCircle, Clock, FileCheck } from "lucide-react"; import { parse, format } from "date-fns"; @@ -20,6 +21,7 @@ import { Button } from "@/components/ui/button"; //creating types out of schema auto generated. type Appointment = z.infer; +type Claim = z.infer; const insertAppointmentSchema = ( AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject @@ -77,6 +79,7 @@ export default function ClaimsPage() { patientId: null, serviceDate: "", }); + const [claimRes, setClaimRes] = useState(null); // Fetch patients const { data: patients = [], isLoading: isLoadingPatients } = useQuery< @@ -215,8 +218,13 @@ export default function ClaimsPage() { // selenium pdf download handler const handleSeleniumPopup = async (actionType: string) => { try { + if (!claimRes?.id || !selectedPatient) { + throw new Error("Missing claim or patient selection"); + } const res = await apiRequest("POST", "/api/claims/selenium/fetchpdf", { action: actionType, + patientId:selectedPatient, + claimId:claimRes.id }); const result = await res.json(); @@ -319,7 +327,8 @@ export default function ClaimsPage() { const res = await apiRequest("POST", "/api/claims/", claimData); return res.json(); }, - onSuccess: () => { + onSuccess: (data) => { + setClaimRes(data); toast({ title: "Claim submitted successfully", variant: "default", diff --git a/package.json b/package.json index f54901f..339f2f3 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "lint": "turbo run lint", "check-types": "turbo run check-types", "format": "prettier --write \"**/*.{ts,tsx,md}\"", - "db:generate": "prisma generate --schema=packages/db/prisma/schema.prisma", + "db:generate": "prisma generate --schema=packages/db/prisma/schema.prisma && ts-node packages/db/scripts/patch-zod-buffer.ts", "db:migrate": "dotenv -e packages/db/.env -- prisma migrate dev --schema=packages/db/prisma/schema.prisma", "db:seed": "prisma db seed --schema=packages/db/prisma/schema.prisma", "setup:env": "shx cp packages/db/prisma/.env.example packages/db/prisma/.env && shx cp apps/Frontend/.env.example apps/Frontend/.env && shx cp apps/Backend/.env.example apps/Backend/.env", diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index e2bf659..218e833 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -52,6 +52,7 @@ model Patient { user User @relation(fields: [userId], references: [id]) appointments Appointment[] claims Claim[] + pdfs ClaimPdf[] } model Appointment { @@ -107,6 +108,7 @@ model Claim { staff Staff? @relation("ClaimStaff", fields: [staffId], references: [id]) serviceLines ServiceLine[] + pdf ClaimPdf[] } model ServiceLine { @@ -130,6 +132,21 @@ model InsuranceCredential { user User @relation(fields: [userId], references: [id], onDelete: Cascade) + @@unique([userId, siteKey]) @@index([userId]) - @@unique([userId, siteKey]) +} + +model ClaimPdf { + id Int @id @default(autoincrement()) + patientId Int + claimId Int? + filename String + pdfData Bytes + uploadedAt DateTime @default(now()) + + patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade) + claim Claim? @relation(fields: [claimId], references: [id], onDelete: Cascade) + + @@index([patientId]) + @@index([claimId]) } diff --git a/packages/db/scripts/patch-zod-buffer.ts b/packages/db/scripts/patch-zod-buffer.ts new file mode 100644 index 0000000..e478764 --- /dev/null +++ b/packages/db/scripts/patch-zod-buffer.ts @@ -0,0 +1,15 @@ +import fs from 'fs'; +import path from 'path'; + +const dir = path.resolve(__dirname, '../shared/schemas/objects'); + +fs.readdirSync(dir).forEach(file => { + if (!file.endsWith('.schema.ts')) return; + const full = path.join(dir, file); + let content = fs.readFileSync(full, 'utf8'); + if (content.includes('z.instanceof(Buffer)') && !content.includes("import { Buffer")) { + content = `import { Buffer } from 'buffer';\n` + content; + fs.writeFileSync(full, content); + console.log('Patched:', file); + } +}); diff --git a/packages/db/usedSchemas/index.ts b/packages/db/usedSchemas/index.ts index c7f9cec..0eaa6fd 100644 --- a/packages/db/usedSchemas/index.ts +++ b/packages/db/usedSchemas/index.ts @@ -4,4 +4,5 @@ export * from '../shared/schemas/objects/PatientUncheckedCreateInput.schema'; export * from '../shared/schemas/objects/UserUncheckedCreateInput.schema'; export * from '../shared/schemas/objects/StaffUncheckedCreateInput.schema' export * from '../shared/schemas/objects/ClaimUncheckedCreateInput.schema' -export * from '../shared/schemas/objects/InsuranceCredentialUncheckedCreateInput.schema' \ No newline at end of file +export * from '../shared/schemas/objects/InsuranceCredentialUncheckedCreateInput.schema' +export * from '../shared/schemas/objects/ClaimPdfUncheckedCreateInput.schema' \ No newline at end of file