From 46d463d4a9e8e46bb42d25a4a830992efeac1513 Mon Sep 17 00:00:00 2001 From: Potenz Date: Sun, 31 Aug 2025 00:43:33 +0530 Subject: [PATCH] feat(ocr-paymentpage) - frontend recieving content and displaying initial, not editable --- apps/Backend/src/routes/index.ts | 2 +- .../components/payments/payment-ocr-block.tsx | 276 ++++++++++++++++++ apps/Frontend/src/pages/payments-page.tsx | 205 +------------ apps/PaymentOCRService/.env.example | 2 +- apps/PaymentOCRService/main.py | 8 +- 5 files changed, 284 insertions(+), 209 deletions(-) create mode 100644 apps/Frontend/src/components/payments/payment-ocr-block.tsx diff --git a/apps/Backend/src/routes/index.ts b/apps/Backend/src/routes/index.ts index ffe1e93..98f3394 100644 --- a/apps/Backend/src/routes/index.ts +++ b/apps/Backend/src/routes/index.ts @@ -4,7 +4,7 @@ import appointmentsRoutes from "./appointments"; import usersRoutes from "./users"; import staffsRoutes from "./staffs"; import claimsRoutes from "./claims"; -import patientDataExtractionRoutes from "./patientdataExtraction"; +import patientDataExtractionRoutes from "./patientDataExtraction"; import insuranceCredsRoutes from "./insuranceCreds"; import documentsRoutes from "./documents"; import insuranceEligibilityRoutes from "./insuranceEligibility"; diff --git a/apps/Frontend/src/components/payments/payment-ocr-block.tsx b/apps/Frontend/src/components/payments/payment-ocr-block.tsx new file mode 100644 index 0000000..dd73c5f --- /dev/null +++ b/apps/Frontend/src/components/payments/payment-ocr-block.tsx @@ -0,0 +1,276 @@ +// PaymentOCRBlock.tsx +import * as React from "react"; + +// If you're using shadcn/ui and lucide-react, keep these. +// Otherwise swap for your own components/icons. +import { Card, CardContent } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Upload, Image as ImageIcon, X } from "lucide-react"; +import { useMutation } from "@tanstack/react-query"; +import { apiRequest } from "@/lib/queryClient"; +import { toast } from "@/hooks/use-toast"; + +type Row = Record; + +export default function PaymentOCRBlock() { + // UI state + const [uploadedImage, setUploadedImage] = React.useState(null); + const [uploadedImages, setUploadedImages] = React.useState([]); + const [isDragging, setIsDragging] = React.useState(false); + const [isExtracting, setIsExtracting] = React.useState(false); + const [rows, setRows] = React.useState([]); + const [error, setError] = React.useState(null); + + //Mutation + const extractPaymentOCR = useMutation({ + mutationFn: async (files: File[]) => { + const formData = new FormData(); + files.forEach((file) => formData.append("files", file, file.name)); + + const res = await apiRequest( + "POST", + "/api/payment-ocr/extract", + formData + ); + + if (!res.ok) throw new Error("Failed to extract payment data"); + const data = (await res.json()) as { rows: Row[] } | Row[]; + return Array.isArray(data) ? data : data.rows; + }, + onSuccess: (rows) => { + setRows(rows); + }, + onError: (error: any) => { + toast({ + title: "Error", + description: `Failed to extract payment data: ${error.message}`, + variant: "destructive", + }); + }, + }); + + // ---- handlers (all in this file) ----------------------------------------- + const handleImageSelect = (e: React.ChangeEvent) => { + const files = e.target.files; + if (!files || !files.length) return; + const list = Array.from(files); + setUploadedImage(list[0] as File); // preview first + setUploadedImages(list); + setError(null); + }; + + const handleImageDrop = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + const list = Array.from(e.dataTransfer.files || []).filter((f) => + f.type.startsWith("image/") + ); + if (!list.length) { + setError("Please drop image files (JPG/PNG/TIFF/BMP)."); + return; + } + setUploadedImage(list[0] as File); + + setUploadedImages(list); + setError(null); + }; + + const removeUploadedImage = () => { + setUploadedImage(null); + setUploadedImages([]); + setRows([]); + setError(null); + }; + + const handleExtract = () => { + if (!uploadedImages.length) return; + extractPaymentOCR.mutate(uploadedImages); + }; + + // ---- render --------------------------------------------------------------- + return ( +
+
+

+ Payment Document OCR +

+
+ + + +
+
{ + e.preventDefault(); + setIsDragging(true); + }} + onDragLeave={() => setIsDragging(false)} + onClick={() => + document.getElementById("image-upload-input")?.click() + } + > + {uploadedImage ? ( +
+
+ +
+

+ {uploadedImage.name} +

+

+ {(uploadedImage.size / 1024 / 1024).toFixed(2)} MB +

+
+ +
+ {isExtracting && ( +
+ Extracting payment information... +
+ )} +
+ ) : ( +
+ +
+

+ Upload Payment Document +

+

+ Drag and drop image(s) or click to browse +

+ +
+

+ Supported formats: JPG, PNG, TIFF, BMP • Max size: 10MB each +

+
+ )} + + +
+ +
+ +
+ + {/* Results */} + {error &&

{error}

} + +
+
+
+
+ ); +} + +// --------------------- helpers (kept in same file) ---------------------------- + +function OCRTable({ rows }: { rows: Row[] }) { + if (!rows?.length) return null; + + // prefer Excel-like order if present, then append any extra columns + const desired = [ + "Patient Name", + "Patient ID", + "ICN", + "CDT Code", + "Tooth", + "Date SVC", + "Billed Amount", + "Allowed Amount", + "Paid Amount", + "Extraction Success", + "Source File", + ]; + const dynamicCols = Array.from( + rows.reduce>((acc, r) => { + Object.keys(r || {}).forEach((k) => acc.add(k)); + return acc; + }, new Set()) + ); + + const columns = [ + ...desired.filter((c) => dynamicCols.includes(c)), + ...dynamicCols.filter((c) => !desired.includes(c)), + ]; + + return ( +
+ + + + {columns.map((c) => ( + + ))} + + + + {rows.map((r, i) => ( + + {columns.map((c) => ( + + ))} + + ))} + +
+ {c} +
+ {formatCell(r[c])} +
+
+ ); +} + +function formatCell(v: unknown) { + if (v === null || v === undefined) return ""; + if (typeof v === "boolean") return v ? "Yes" : "No"; + return String(v); +} + +async function safeJson(resp: Response) { + try { + return await resp.json(); + } catch { + return null; + } +} diff --git a/apps/Frontend/src/pages/payments-page.tsx b/apps/Frontend/src/pages/payments-page.tsx index b399df1..275d8a9 100644 --- a/apps/Frontend/src/pages/payments-page.tsx +++ b/apps/Frontend/src/pages/payments-page.tsx @@ -6,9 +6,7 @@ import { CardContent, CardDescription, } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { useToast } from "@/hooks/use-toast"; -import { DollarSign, Upload, Image, X } from "lucide-react"; +import { DollarSign } from "lucide-react"; import { Select, SelectContent, @@ -18,95 +16,10 @@ import { } from "@/components/ui/select"; import PaymentsRecentTable from "@/components/payments/payments-recent-table"; import PaymentsOfPatientModal from "@/components/payments/payments-of-patient-table"; +import PaymentOCRBlock from "@/components/payments/payment-ocr-block"; export default function PaymentsPage() { - const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [paymentPeriod, setPaymentPeriod] = useState("all-time"); - const [uploadedImage, setUploadedImage] = useState(null); - const [isExtracting, setIsExtracting] = useState(false); - const [isDragging, setIsDragging] = useState(false); - const [editableData, setEditableData] = useState([]); - - const { toast } = useToast(); - - const toggleMobileMenu = () => { - setIsMobileMenuOpen(!isMobileMenuOpen); - }; - - // Image upload handlers for OCR - const handleImageDrop = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setIsDragging(false); - - const files = e.dataTransfer.files; - if (files && files[0]) { - const file = files[0]; - if (file.type.startsWith("image/")) { - setUploadedImage(file); - } else { - toast({ - title: "Invalid file type", - description: "Please upload an image file (JPG, PNG, etc.)", - variant: "destructive", - }); - } - } - }; - - const handleImageSelect = (e: React.ChangeEvent) => { - const files = e.target.files; - if (files && files[0]) { - const file = files[0]; - setUploadedImage(file); - } - }; - - const handleOCRExtraction = async (file: File) => { - setIsExtracting(true); - - try { - // Create FormData for image upload - const formData = new FormData(); - formData.append("image", file); - - // Simulate OCR extraction process - await new Promise((resolve) => setTimeout(resolve, 2000)); - } finally { - setIsExtracting(false); - } - }; - - const removeUploadedImage = () => { - setUploadedImage(null); - }; - - const updateEditableData = ( - index: number, - field: string, - value: string | number - ) => { - setEditableData((prev) => { - const updated = [...prev]; - updated[index] = { ...updated[index], [field]: value }; - return updated; - }); - }; - - const deleteRow = (index: number) => { - setEditableData((prev) => prev.filter((_, i) => i !== index)); - toast({ - title: "Row Deleted", - description: `Payment record has been removed`, - }); - }; - - const saveRow = (index: number) => { - toast({ - title: "Row Saved", - description: `Changes to payment record ${index + 1} have been saved`, - }); - }; return (
@@ -192,118 +105,8 @@ export default function PaymentsPage() {
- {/* OCR Image Upload Section - not working rn*/} -
-
-

- Payment Document OCR -

-
- - - -
-
{ - e.preventDefault(); - setIsDragging(true); - }} - onDragLeave={() => setIsDragging(false)} - onClick={() => - document.getElementById("image-upload-input")?.click() - } - > - {uploadedImage ? ( -
-
- -
-

- {uploadedImage.name} -

-

- {(uploadedImage.size / 1024 / 1024).toFixed(2)} MB -

-
- -
- {isExtracting && ( -
- Extracting payment information... -
- )} -
- ) : ( -
- -
-

- Upload Payment Document -

-

- Drag and drop an image or click to browse -

- -
-

- Supported formats: JPG, PNG, GIF • Max size: 10MB -

-
- )} - - -
- -
- -
-
-
-
-
+ {/* OCR Image Upload Section*/} + {/* Recent Payments table */} diff --git a/apps/PaymentOCRService/.env.example b/apps/PaymentOCRService/.env.example index 2b436fd..6521493 100644 --- a/apps/PaymentOCRService/.env.example +++ b/apps/PaymentOCRService/.env.example @@ -1,3 +1,3 @@ -GOOGLE_APPLICATION_CREDENTIALS=google-credentials.json +GOOGLE_APPLICATION_CREDENTIALS=google_credentials.json HOST="0.0.0.0" PORT="5003" \ No newline at end of file diff --git a/apps/PaymentOCRService/main.py b/apps/PaymentOCRService/main.py index 502232e..925284a 100644 --- a/apps/PaymentOCRService/main.py +++ b/apps/PaymentOCRService/main.py @@ -7,14 +7,10 @@ import os import asyncio from dotenv import load_dotenv -load_dotenv() # loads .env (GOOGLE_APPLICATION_CREDENTIALS, HOST, PORT, etc.) +load_dotenv() -# Your adapter that calls the pipeline from complete_pipeline_adapter import process_images_to_rows,rows_to_csv_bytes -# ------------------------------------------------- -# App + concurrency controls (similar to your other app) -# ------------------------------------------------- app = FastAPI( title="Payment OCR Services API", description="FastAPI wrapper around the OCR pipeline (Google Vision + deskew + line grouping + extraction).", @@ -92,7 +88,7 @@ async def extract_json(files: List[UploadFile] = File(...)): try: blobs = [await f.read() for f in files] names = [f.filename or "upload.bin" for f in files] - rows = process_images_to_rows(blobs, names) # calls your pipeline + rows = process_images_to_rows(blobs, names) # calls pipeline return JSONResponse(content={"rows": rows}) except Exception as e: raise HTTPException(status_code=500, detail=f"Processing error: {e}")