// PaymentOCRBlock.tsx import * as React from "react"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; 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"; import { useReactTable, getCoreRowModel, flexRender, ColumnDef, } from "@tanstack/react-table"; import { convertOCRDate } from "@/utils/dateUtils"; // ---------------- Types ---------------- type Row = { __id: number } & Record; export default function PaymentOCRBlock() { // UI state 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 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: (data) => { // Remove unwanted keys before using the data const cleaned = data.map((row) => { const { ["Extraction Success"]: _, ["Source File"]: __, ...rest } = row; return rest; }); const withIds: Row[] = cleaned.map((r, i) => ({ ...r, __id: i })); setRows(withIds); const allKeys = Array.from( cleaned.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 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); }; 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; } if (list.length > 10) { setError("You can only upload up to 10 images."); return; } setUploadedImages(list); 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); }; const handleSave = async () => { try { const skipped: string[] = []; const payload = rows .map((row, idx) => { const patientName = row["Patient Name"]; const patientId = row["Patient ID"]; const procedureCode = row["CDT Code"]; if (!patientName || !patientId || !procedureCode) { skipped.push(`Row ${idx + 1} (missing name/id/procedureCode)`); return null; } return { patientName, insuranceId: patientId, icn: row["ICN"] ?? null, procedureCode: row["CDT Code"], toothNumber: row["Tooth"] ?? null, toothSurface: row["Surface"] ?? null, procedureDate: row["Date SVC"] ?? null, totalBilled: Number(row["Billed Amount"] ?? 0), totalAllowed: Number(row["Allowed Amount"] ?? 0), totalPaid: Number(row["Paid Amount"] ?? 0), sourceFile: row["Source File"] ?? null, }; }) .filter((r) => r !== null); if (skipped.length > 0) { toast({ title: "Some rows skipped, because of either no patient Name or MemberId given.", description: skipped.join(", "), variant: "destructive", }); } if (payload.length === 0) { toast({ title: "Error", description: "No valid rows to save", variant: "destructive", }); return; } const res = await apiRequest("POST", "/api/payments/full-ocr-import", { rows: payload, }); if (!res.ok) throw new Error("Failed to save OCR payments"); toast({ title: "Saved", description: "OCR rows saved successfully" }); } catch (err: any) { toast({ title: "Error", description: err.message, variant: "destructive", }); } }; //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 (

Payment Document OCR

{/* 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

))}
) : (

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