// PaymentOCRBlock.tsx import * as React from "react"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Image as ImageIcon, X, Plus } from "lucide-react"; import { useMutation } from "@tanstack/react-query"; import { apiRequest, queryClient } from "@/lib/queryClient"; import { toast } from "@/hooks/use-toast"; import { useReactTable, getCoreRowModel, flexRender, } from "@tanstack/react-table"; import { QK_PAYMENTS_RECENT_BASE } from "@/components/payments/payments-recent-table"; import { QK_PATIENTS_BASE } from "@/components/patients/patient-table"; import { MultipleFileUploadZone, } from "../file-upload/multiple-file-upload-zone"; export default function PaymentOCRBlock() { //Config const MAX_FILES = 10; const ACCEPTED_FILE_TYPES = "image/jpeg,image/jpg,image/png,image/webp"; const TITLE = "Payment Document OCR"; const DESCRIPTION = "You can upload up to 10 files. Allowed types: JPG, PNG, WEBP."; // FILE/ZONE state const uploadZoneRef = React.useRef(null); const [filesForUI, setFilesForUI] = React.useState([]); // reactive UI only const [isUploading, setIsUploading] = React.useState(false); // forwarded to zone const [isExtracting, setIsExtracting] = React.useState(false); // extracted rows shown only inside modal const [rows, setRows] = React.useState([]); const [modalColumns, setModalColumns] = React.useState([]); const [showModal, setShowModal] = React.useState(false); const [error, setError] = React.useState(null); //Mutation const extractPaymentOCR = useMutation({ mutationFn: async (files) => { 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()); 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 = 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())); setModalColumns(allKeys); setIsExtracting(false); setShowModal(true); }, onError: (error) => { toast({ title: "Error", description: `Failed to extract payment data: ${error.message}`, variant: "destructive", }); setIsExtracting(false); }, }); // ---- handlers (all in this file) ----------------------------------------- // Called by zone when its internal list changes (keeps parent UI reactive) const handleZoneFilesChange = React.useCallback((files) => { setFilesForUI(files); setError(null); }, []); // Remove a single file by asking the zone to remove it (zone exposes removeFile) const removeUploadedFile = React.useCallback((index) => { uploadZoneRef.current?.removeFile(index); // zone will call onFilesChange and update filesForUI automatically }, []); // Extract: read files from zone via ref and call mutation const handleExtract = () => { const files = uploadZoneRef.current?.getFiles() ?? []; if (!files.length) { setError("Please upload at least one file to extract."); return; } setIsExtracting(true); extractPaymentOCR.mutate(files); }; const handleSave = async () => { try { const skipped = []; 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" }); // 🔄 REFRESH both tables (all pages/filters) await Promise.all([ queryClient.invalidateQueries({ queryKey: QK_PAYMENTS_RECENT_BASE }), // all recent payments queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE }), // recent patients list ]); // ✅ CLEAR UI: reset zone + modal + rows uploadZoneRef.current?.reset(); setFilesForUI([]); setRows([]); setModalColumns([]); setError(null); setIsExtracting(false); setShowModal(false); } catch (err) { toast({ title: "Error", description: err.message, variant: "destructive", }); } }; return (
Uploaded ({filesForUI.length}/{MAX_FILES})
{file.name}
{(file.size / 1024 / 1024).toFixed(2)} MB
{error}
}| {flexRender(header.column.columnDef.header, header.getContext())} | ))}Actions |
|---|---|
| {flexRender(cell.column.columnDef.cell, cell.getContext())} | ))}