pdf upload checkpoint1
This commit is contained in:
@@ -30,6 +30,11 @@ npm run db:generate
|
|||||||
npm run db:migrate
|
npm run db:migrate
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- Generate the db types:
|
||||||
|
```sh
|
||||||
|
npm run db:generate
|
||||||
|
```
|
||||||
|
|
||||||
- seed the db if:
|
- seed the db if:
|
||||||
```sh
|
```sh
|
||||||
npm run db:seed
|
npm run db:seed
|
||||||
|
|||||||
67
apps/Backend/src/routes/claim-pdf.ts
Normal file
67
apps/Backend/src/routes/claim-pdf.ts
Normal file
@@ -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;
|
||||||
@@ -6,7 +6,6 @@ import { ClaimUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
|
|||||||
import multer from "multer";
|
import multer from "multer";
|
||||||
import { forwardToSeleniumAgent, forwardToSeleniumAgent2 } from "../services/seleniumClient";
|
import { forwardToSeleniumAgent, forwardToSeleniumAgent2 } from "../services/seleniumClient";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs";
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
@@ -96,6 +95,14 @@ router.post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try{
|
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();
|
const result = await forwardToSeleniumAgent2();
|
||||||
|
|
||||||
if (result.status !== "success") {
|
if (result.status !== "success") {
|
||||||
@@ -104,17 +111,14 @@ router.post(
|
|||||||
|
|
||||||
const pdfUrl = result.pdf_url;
|
const pdfUrl = result.pdf_url;
|
||||||
const filename = path.basename(new URL(pdfUrl).pathname);
|
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" });
|
const pdfResponse = await axios.get(pdfUrl, { responseType: "arraybuffer" });
|
||||||
fs.writeFileSync(filePath, pdfResponse.data);
|
|
||||||
|
await storage.createClaimPdf(
|
||||||
|
parsedPatientId,
|
||||||
|
parsedClaimId,
|
||||||
|
filename,
|
||||||
|
pdfResponse.data
|
||||||
|
);
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
StaffUncheckedCreateInputObjectSchema,
|
StaffUncheckedCreateInputObjectSchema,
|
||||||
ClaimUncheckedCreateInputObjectSchema,
|
ClaimUncheckedCreateInputObjectSchema,
|
||||||
InsuranceCredentialUncheckedCreateInputObjectSchema,
|
InsuranceCredentialUncheckedCreateInputObjectSchema,
|
||||||
|
ClaimPdfUncheckedCreateInputObjectSchema
|
||||||
} from "@repo/db/usedSchemas";
|
} from "@repo/db/usedSchemas";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -145,6 +146,15 @@ type ClaimWithServiceLines = Claim & {
|
|||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// claim types:
|
||||||
|
type ClaimPdf = z.infer<typeof ClaimPdfUncheckedCreateInputObjectSchema>;
|
||||||
|
|
||||||
|
export interface ClaimPdfMetadata {
|
||||||
|
id: number;
|
||||||
|
filename: string;
|
||||||
|
uploadedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IStorage {
|
export interface IStorage {
|
||||||
// User methods
|
// User methods
|
||||||
getUser(id: number): Promise<User | undefined>;
|
getUser(id: number): Promise<User | undefined>;
|
||||||
@@ -211,6 +221,27 @@ export interface IStorage {
|
|||||||
userId: number,
|
userId: number,
|
||||||
siteKey: string
|
siteKey: string
|
||||||
): Promise<InsuranceCredential | null>;
|
): Promise<InsuranceCredential | null>;
|
||||||
|
|
||||||
|
// Claim PDF Methods
|
||||||
|
createClaimPdf(
|
||||||
|
patientId: number,
|
||||||
|
claimId: number,
|
||||||
|
filename: string,
|
||||||
|
pdfData: Buffer
|
||||||
|
): Promise<ClaimPdf>;
|
||||||
|
|
||||||
|
getClaimPdfById(id: number): Promise<ClaimPdf | undefined>;
|
||||||
|
|
||||||
|
getAllClaimPdfs(): Promise<ClaimPdfMetadata[]>;
|
||||||
|
|
||||||
|
getRecentClaimPdfs(limit: number, offset: number): Promise<ClaimPdfMetadata[]>;
|
||||||
|
|
||||||
|
deleteClaimPdf(id: number): Promise<boolean>;
|
||||||
|
|
||||||
|
updateClaimPdf(
|
||||||
|
id: number,
|
||||||
|
updates: Partial<Pick<ClaimPdf, "filename" | "pdfData">>
|
||||||
|
): Promise<ClaimPdf | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const storage: IStorage = {
|
export const storage: IStorage = {
|
||||||
@@ -474,5 +505,66 @@ export const storage: IStorage = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// pdf claims
|
||||||
|
async createClaimPdf(
|
||||||
|
patientId,
|
||||||
|
claimId,
|
||||||
|
filename,
|
||||||
|
pdfData
|
||||||
|
): Promise<ClaimPdf> {
|
||||||
|
return db.claimPdf.create({
|
||||||
|
data: {
|
||||||
|
patientId,
|
||||||
|
claimId,
|
||||||
|
filename,
|
||||||
|
pdfData,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async getClaimPdfById(id: number): Promise<ClaimPdf | undefined> {
|
||||||
|
const pdf = await db.claimPdf.findUnique({ where: { id } });
|
||||||
|
return pdf ?? undefined;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getAllClaimPdfs(): Promise<ClaimPdfMetadata[]> {
|
||||||
|
return db.claimPdf.findMany({
|
||||||
|
select: { id: true, filename: true, uploadedAt: true },
|
||||||
|
orderBy: { uploadedAt: "desc" },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async getRecentClaimPdfs(limit: number, offset: number): Promise<ClaimPdfMetadata[]> {
|
||||||
|
return db.claimPdf.findMany({
|
||||||
|
skip: offset,
|
||||||
|
take: limit,
|
||||||
|
orderBy: { uploadedAt: "desc" },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
filename: true,
|
||||||
|
uploadedAt: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteClaimPdf(id: number): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await db.claimPdf.delete({ where: { id } });
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateClaimPdf(
|
||||||
|
id: number,
|
||||||
|
updates: Partial<Pick<ClaimPdf, "filename" | "pdfData">>
|
||||||
|
): Promise<ClaimPdf | undefined> {
|
||||||
|
try {
|
||||||
|
const updated = await db.claimPdf.update({ where: { id }, data: updates });
|
||||||
|
return updated;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useAuth } from "@/hooks/use-auth";
|
|||||||
import {
|
import {
|
||||||
PatientUncheckedCreateInputObjectSchema,
|
PatientUncheckedCreateInputObjectSchema,
|
||||||
AppointmentUncheckedCreateInputObjectSchema,
|
AppointmentUncheckedCreateInputObjectSchema,
|
||||||
|
ClaimUncheckedCreateInputObjectSchema
|
||||||
} from "@repo/db/usedSchemas";
|
} from "@repo/db/usedSchemas";
|
||||||
import { AlertCircle, CheckCircle, Clock, FileCheck } from "lucide-react";
|
import { AlertCircle, CheckCircle, Clock, FileCheck } from "lucide-react";
|
||||||
import { parse, format } from "date-fns";
|
import { parse, format } from "date-fns";
|
||||||
@@ -20,6 +21,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
|
|
||||||
//creating types out of schema auto generated.
|
//creating types out of schema auto generated.
|
||||||
type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>;
|
type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>;
|
||||||
|
type Claim = z.infer<typeof ClaimUncheckedCreateInputObjectSchema>;
|
||||||
|
|
||||||
const insertAppointmentSchema = (
|
const insertAppointmentSchema = (
|
||||||
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||||
@@ -77,6 +79,7 @@ export default function ClaimsPage() {
|
|||||||
patientId: null,
|
patientId: null,
|
||||||
serviceDate: "",
|
serviceDate: "",
|
||||||
});
|
});
|
||||||
|
const [claimRes, setClaimRes] = useState<Claim | null>(null);
|
||||||
|
|
||||||
// Fetch patients
|
// Fetch patients
|
||||||
const { data: patients = [], isLoading: isLoadingPatients } = useQuery<
|
const { data: patients = [], isLoading: isLoadingPatients } = useQuery<
|
||||||
@@ -215,8 +218,13 @@ export default function ClaimsPage() {
|
|||||||
// selenium pdf download handler
|
// selenium pdf download handler
|
||||||
const handleSeleniumPopup = async (actionType: string) => {
|
const handleSeleniumPopup = async (actionType: string) => {
|
||||||
try {
|
try {
|
||||||
|
if (!claimRes?.id || !selectedPatient) {
|
||||||
|
throw new Error("Missing claim or patient selection");
|
||||||
|
}
|
||||||
const res = await apiRequest("POST", "/api/claims/selenium/fetchpdf", {
|
const res = await apiRequest("POST", "/api/claims/selenium/fetchpdf", {
|
||||||
action: actionType,
|
action: actionType,
|
||||||
|
patientId:selectedPatient,
|
||||||
|
claimId:claimRes.id
|
||||||
});
|
});
|
||||||
const result = await res.json();
|
const result = await res.json();
|
||||||
|
|
||||||
@@ -319,7 +327,8 @@ export default function ClaimsPage() {
|
|||||||
const res = await apiRequest("POST", "/api/claims/", claimData);
|
const res = await apiRequest("POST", "/api/claims/", claimData);
|
||||||
return res.json();
|
return res.json();
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: (data) => {
|
||||||
|
setClaimRes(data);
|
||||||
toast({
|
toast({
|
||||||
title: "Claim submitted successfully",
|
title: "Claim submitted successfully",
|
||||||
variant: "default",
|
variant: "default",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"lint": "turbo run lint",
|
"lint": "turbo run lint",
|
||||||
"check-types": "turbo run check-types",
|
"check-types": "turbo run check-types",
|
||||||
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
"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: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",
|
"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",
|
"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",
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ model Patient {
|
|||||||
user User @relation(fields: [userId], references: [id])
|
user User @relation(fields: [userId], references: [id])
|
||||||
appointments Appointment[]
|
appointments Appointment[]
|
||||||
claims Claim[]
|
claims Claim[]
|
||||||
|
pdfs ClaimPdf[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Appointment {
|
model Appointment {
|
||||||
@@ -107,6 +108,7 @@ model Claim {
|
|||||||
staff Staff? @relation("ClaimStaff", fields: [staffId], references: [id])
|
staff Staff? @relation("ClaimStaff", fields: [staffId], references: [id])
|
||||||
|
|
||||||
serviceLines ServiceLine[]
|
serviceLines ServiceLine[]
|
||||||
|
pdf ClaimPdf[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model ServiceLine {
|
model ServiceLine {
|
||||||
@@ -130,6 +132,21 @@ model InsuranceCredential {
|
|||||||
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@unique([userId, siteKey])
|
||||||
@@index([userId])
|
@@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])
|
||||||
}
|
}
|
||||||
|
|||||||
15
packages/db/scripts/patch-zod-buffer.ts
Normal file
15
packages/db/scripts/patch-zod-buffer.ts
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -4,4 +4,5 @@ export * from '../shared/schemas/objects/PatientUncheckedCreateInput.schema';
|
|||||||
export * from '../shared/schemas/objects/UserUncheckedCreateInput.schema';
|
export * from '../shared/schemas/objects/UserUncheckedCreateInput.schema';
|
||||||
export * from '../shared/schemas/objects/StaffUncheckedCreateInput.schema'
|
export * from '../shared/schemas/objects/StaffUncheckedCreateInput.schema'
|
||||||
export * from '../shared/schemas/objects/ClaimUncheckedCreateInput.schema'
|
export * from '../shared/schemas/objects/ClaimUncheckedCreateInput.schema'
|
||||||
export * from '../shared/schemas/objects/InsuranceCredentialUncheckedCreateInput.schema'
|
export * from '../shared/schemas/objects/InsuranceCredentialUncheckedCreateInput.schema'
|
||||||
|
export * from '../shared/schemas/objects/ClaimPdfUncheckedCreateInput.schema'
|
||||||
Reference in New Issue
Block a user