From f6bd1489e62ac3dc31c13cdd589d8f24150edfbd Mon Sep 17 00:00:00 2001 From: Potenz Date: Tue, 2 Sep 2025 00:54:35 +0530 Subject: [PATCH] feat(ocr paymentpage) - frontend table added --- apps/Frontend/package.json | 4 +- .../components/payments/payment-ocr-block.tsx | 544 ++++++++++++------ 2 files changed, 383 insertions(+), 165 deletions(-) diff --git a/apps/Frontend/package.json b/apps/Frontend/package.json index fa55ee1..d419ce2 100644 --- a/apps/Frontend/package.json +++ b/apps/Frontend/package.json @@ -12,8 +12,6 @@ "dependencies": { "@hookform/resolvers": "^3.10.0", "@jridgewell/trace-mapping": "^0.3.25", - "@react-pdf-viewer/core": "^3.12.0", - "@react-pdf-viewer/default-layout": "^3.12.0", "@radix-ui/react-accordion": "^1.2.4", "@radix-ui/react-alert-dialog": "^1.1.7", "@radix-ui/react-aspect-ratio": "^1.1.3", @@ -41,6 +39,8 @@ "@radix-ui/react-toggle": "^1.1.3", "@radix-ui/react-toggle-group": "^1.1.3", "@radix-ui/react-tooltip": "^1.2.0", + "@react-pdf-viewer/core": "^3.12.0", + "@react-pdf-viewer/default-layout": "^3.12.0", "@replit/vite-plugin-shadcn-theme-json": "^0.0.4", "@repo/db": "*", "@repo/typescript-config": "*", diff --git a/apps/Frontend/src/components/payments/payment-ocr-block.tsx b/apps/Frontend/src/components/payments/payment-ocr-block.tsx index dd73c5f..1376abc 100644 --- a/apps/Frontend/src/components/payments/payment-ocr-block.tsx +++ b/apps/Frontend/src/components/payments/payment-ocr-block.tsx @@ -1,24 +1,31 @@ // 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 { Upload, Image as ImageIcon, X, Plus, Minus } from "lucide-react"; import { useMutation } from "@tanstack/react-query"; import { apiRequest } from "@/lib/queryClient"; import { toast } from "@/hooks/use-toast"; -type Row = Record; +import { + useReactTable, + getCoreRowModel, + flexRender, + ColumnDef, +} from "@tanstack/react-table"; + +// ---------------- Types ---------------- + +type Row = { __id: number } & 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 [columns, setColumns] = React.useState[]>([]); const [error, setError] = React.useState(null); //Mutation @@ -37,24 +44,68 @@ export default function PaymentOCRBlock() { const data = (await res.json()) as { rows: Row[] } | Row[]; return Array.isArray(data) ? data : data.rows; }, - onSuccess: (rows) => { - setRows(rows); + onSuccess: (data) => { + const withIds: Row[] = data.map((r, i) => ({ ...r, __id: i })); + setRows(withIds); + + const allKeys = Array.from( + data.reduce>((acc, row) => { + Object.keys(row).forEach((k) => acc.add(k)); + return acc; + }, new Set()) + ); + + setColumns( + allKeys.map((key) => ({ + id: key, // ✅ unique identifier + header: key, + cell: ({ row }) => ( + { + const newData = [...rows]; + newData[row.index] = { + ...newData[row.index], + __id: newData[row.index]!.__id, + [key]: e.target.value, + }; + setRows(newData); + }} + /> + ), + })) + ); + + setIsExtracting(false); }, + onError: (error: any) => { toast({ title: "Error", description: `Failed to extract payment data: ${error.message}`, variant: "destructive", }); + setIsExtracting(false); }, }); + // ---- Table instance ---- + const table = useReactTable({ + data: rows, + columns, + getCoreRowModel: getCoreRowModel(), + }); + // ---- 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 + const list = Array.from(e.target.files || []); + if (!list.length) return; + if (list.length > 10) { + setError("You can only upload up to 10 images."); + return; + } setUploadedImages(list); setError(null); }; @@ -69,25 +120,56 @@ export default function PaymentOCRBlock() { setError("Please drop image files (JPG/PNG/TIFF/BMP)."); return; } - setUploadedImage(list[0] as File); + if (list.length > 10) { + setError("You can only upload up to 10 images."); + return; + } setUploadedImages(list); setError(null); }; - const removeUploadedImage = () => { - setUploadedImage(null); - setUploadedImages([]); - setRows([]); - setError(null); + const removeUploadedImage = (index: number) => { + setUploadedImages((prev) => { + const next = prev.filter((_, i) => i !== index); + if (next.length === 0) { + setRows([]); + setColumns([]); + setError(null); + } + return next; + }); }; const handleExtract = () => { if (!uploadedImages.length) return; + setIsExtracting(true); extractPaymentOCR.mutate(uploadedImages); }; - // ---- render --------------------------------------------------------------- + const handleSave = () => { + console.log("Saving edited rows:", rows); + toast({ + title: "Saved", + description: "Edited OCR results are ready to be sent to the database.", + }); + // Here can POST `rows` to your backend API for DB save + }; + + //rows helper + + const handleAddRow = () => { + const newRow: Row = { __id: rows.length }; + columns.forEach((c) => { + if (c.id) newRow[c.id] = ""; + }); + setRows((prev) => [...prev, newRow]); + }; + + const handleDeleteRow = (index: number) => { + setRows((prev) => prev.filter((_, i) => i !== index)); + }; + return (
@@ -97,180 +179,316 @@ export default function PaymentOCRBlock() {
- -
-
{ - e.preventDefault(); - setIsDragging(true); - }} - onDragLeave={() => setIsDragging(false)} - onClick={() => - document.getElementById("image-upload-input")?.click() - } - > - {uploadedImage ? ( -
-
- -
-

- {uploadedImage.name} -

-

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

+ + {/* Upload block */} +
{ + e.preventDefault(); + setIsDragging(true); + }} + onDragLeave={() => setIsDragging(false)} + onClick={() => + document.getElementById("image-upload-input")?.click() + } + > + {uploadedImages.length ? ( +
+ {uploadedImages.map((file, idx) => ( +
+
+ +
+

+ {file.name} +

+

+ {(file.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 + ))} +

+ ) : ( +
+ +
+

+ Upload Payment Documents

+

+ Drag and drop up to 10 images or click to browse +

+
- )} +

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

+
+ )} + +
- -
+ {/* Extract */} + +
+ +
+ + {/* Results Table */} + + {error &&

{error}

} + + {rows.length > 0 && ( +
+ {/* Row/Column control buttons */} +
+ +
+ + {/* Table */} + +
+ + + {table.getHeaderGroups().map((hg) => ( + + {hg.headers.map((header) => ( + + ))} + + + ))} + + + {table.getRowModel().rows.map((row, rowIndex) => ( + + {row.getVisibleCells().map((cell) => { + const colId = cell.column.id; // ✅ key for field + return ( + + ); + })} + + + ))} + +
+ {flexRender( + header.column.columnDef.header, + header.getContext() + )} + Actions
+ { + const newData = [...rows]; + newData[rowIndex] = { + ...newData[rowIndex], + __id: newData[rowIndex]!.__id, // keep id + [colId]: e.target.value, + }; + setRows(newData); + }} + /> + + +
+
-
- - {/* Results */} - {error &&

{error}

} - -
+ )}
); } -// --------------------- helpers (kept in same file) ---------------------------- +// --------------------- Editable OCRTable ---------------------------- -function OCRTable({ rows }: { rows: Row[] }) { - if (!rows?.length) return null; +// function OCRTable({ +// rows, +// setRows, +// }: { +// rows: Row[]; +// setRows: React.Dispatch>; +// }) { +// const [columns, setColumns] = React.useState([]); - // 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()) - ); +// // Initialize columns once when rows come in +// React.useEffect(() => { +// if (rows.length && columns.length === 0) { +// const dynamicCols = Array.from( +// rows.reduce>((acc, r) => { +// Object.keys(r || {}).forEach((k) => acc.add(k)); +// return acc; +// }, new Set()) +// ); +// setColumns(dynamicCols); +// } +// }, [rows]); - const columns = [ - ...desired.filter((c) => dynamicCols.includes(c)), - ...dynamicCols.filter((c) => !desired.includes(c)), - ]; +// if (!rows?.length) return null; - return ( -
- - - - {columns.map((c) => ( - - ))} - - - - {rows.map((r, i) => ( - - {columns.map((c) => ( - - ))} - - ))} - -
- {c} -
- {formatCell(r[c])} -
-
- ); -} +// // ---------------- column handling ---------------- +// const handleColumnNameChange = (colIdx: number, value: string) => { +// // Just update local columns state for typing responsiveness +// setColumns((prev) => prev.map((c, i) => (i === colIdx ? value : c))); +// }; -function formatCell(v: unknown) { - if (v === null || v === undefined) return ""; - if (typeof v === "boolean") return v ? "Yes" : "No"; - return String(v); -} +// const commitColumnRename = (colIdx: number) => { +// const oldName = Object.keys(rows[0] ?? {})[colIdx]; +// const newName = columns[colIdx]; +// if (!oldName || !newName || oldName === newName) return; -async function safeJson(resp: Response) { - try { - return await resp.json(); - } catch { - return null; - } -} +// const updated = rows.map((r) => { +// const { [oldName]: oldVal, ...rest } = r; +// return { ...rest, [newName]: oldVal }; +// }); +// setRows(updated); +// }; + +// // ---------------- row/col editing ---------------- +// const addColumn = () => { +// const newColName = `New Column ${columns.length + 1}`; +// setColumns((prev) => [...prev, newColName]); +// const updated = rows.map((r) => ({ ...r, [newColName]: "" })); +// setRows(updated); +// }; + +// const addRow = () => { +// const newRow: Row = {}; +// columns.forEach((c) => (newRow[c] = "")); +// setRows([...rows, newRow]); +// }; + +// const handleCellChange = (rowIdx: number, col: string, value: string) => { +// const updated = rows.map((r, i) => +// i === rowIdx ? { ...r, [col]: value } : r +// ); +// setRows(updated); +// }; + +// // ---------------- render ---------------- +// return ( +//
+// +// +// +// {columns.map((c, idx) => ( +// +// ))} +// +// +// +// +// {rows.map((r, i) => ( +// +// {columns.map((c) => ( +// +// ))} +// +// ))} +// +// +// +// +//
+// handleColumnNameChange(idx, e.target.value)} +// onBlur={() => commitColumnRename(idx)} +// onKeyDown={(e) => { +// if (e.key === "Enter") { +// e.currentTarget.blur(); // commit rename +// } +// }} +// /> +// +// +//
+// handleCellChange(i, c, e.target.value)} +// /> +//
+// +//
+//
+// ); +// }