import * as React from "react"; import * as XLSX from "xlsx"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { FileText, X, Download, DatabaseIcon } from "lucide-react"; import { useMutation } from "@tanstack/react-query"; import { apiRequest, queryClient } from "@/lib/queryClient"; import { toast } from "@/hooks/use-toast"; import { QK_PAYMENTS_RECENT_BASE } from "@/components/payments/payments-recent-table"; import { MultipleFileUploadZone, } from "../file-upload/multiple-file-upload-zone"; const COLUMNS = [ { key: "Patient Name", label: "Patient Name" }, { key: "Member #", label: "Member #" }, { key: "ICN", label: "ICN" }, { key: "Submitted Code", label: "Submitted Code" }, { key: "Paid Code", label: "Paid Code" }, { key: "Tooth", label: "Tooth" }, { key: "Date of Service", label: "Date of Service" }, { key: "Submitted Amount", label: "Submitted ($)" }, { key: "Allowed Amount", label: "Allowed ($)" }, { key: "Paid Amount", label: "Paid ($)" }, ]; const SUMMARY_FIELDS = [ "Source File", "Payee ID", "Business NPI", "Run #", "RA #", "RA Date", "Claim Detail Amount", "Claim Adjustment Amount", "Misc. Adjustment Amount", "Payment Amount", ]; // Convert a string value to a number if it looks numeric, otherwise keep as string. // Handles: "36.00" → 36, "$14,369.00" → 14369, "($3,107.39)" → -3107.39 function toExcelValue(val) { if (!val) return ""; const stripped = val .replace(/\$/g, "") .replace(/,/g, "") .replace(/^\((.+)\)$/, "-$1") // (3,107.39) → -3107.39 .trim(); const num = Number(stripped); return stripped !== "" && !isNaN(num) ? num : val; } function downloadExcel(rows, headers, sourceFileName) { const wb = XLSX.utils.book_new(); // Sheet 1 — RA Summary (one row per uploaded PDF) if (headers.length > 0) { const summaryData = headers.map((h) => { const out = {}; SUMMARY_FIELDS.forEach((f) => { out[f] = toExcelValue(h[f] ?? ""); }); return out; }); const wsSummary = XLSX.utils.json_to_sheet(summaryData, { header: SUMMARY_FIELDS, }); XLSX.utils.book_append_sheet(wb, wsSummary, "RA Summary"); } // Sheet 2 — Payment Data (one row per ICN) const data = rows.map((row) => { const out = {}; COLUMNS.forEach(({ key, label }) => { out[label] = toExcelValue(row[key] ?? ""); }); return out; }); const wsData = XLSX.utils.json_to_sheet(data, { header: COLUMNS.map((c) => c.label), }); XLSX.utils.book_append_sheet(wb, wsData, "Payment Data"); const name = sourceFileName.replace(/\.pdf$/i, "") || "payment_extract"; XLSX.writeFile(wb, `${name}.xlsx`); } export default function PaymentUploadDocumentsBlock() { const MAX_FILES = 10; const ACCEPTED_FILE_TYPES = "application/pdf"; const uploadZoneRef = React.useRef(null); const [filesForUI, setFilesForUI] = React.useState([]); const [isUploading] = React.useState(false); const [error, setError] = React.useState(null); // ── shared extract helper ──────────────────────────────────────────────── const extractData = async (files) => { const formData = new FormData(); files.forEach((file) => formData.append("files", file, file.name)); const res = await apiRequest("POST", "/api/payment-pdf/extract", formData); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err?.error || "Failed to extract PDF data"); } const data = (await res.json()); return { rows: data.rows ?? [], headers: data.headers ?? [] }; }; const getFiles = () => { const files = uploadZoneRef.current?.getFiles() ?? []; if (!files.length) { setError("Please upload at least one PDF file."); return null; } setError(null); return files; }; // ── Extract & Download ─────────────────────────────────────────────────── const downloadMutation = useMutation({ mutationFn: async (files) => { const { rows, headers } = await extractData(files); return { rows, headers, sourceName: files[0]?.name ?? "payment_extract" }; }, onSuccess: ({ rows, headers, sourceName }) => { if (rows.length === 0) { toast({ title: "No data", description: "No rows were extracted from the PDF.", variant: "destructive" }); return; } downloadExcel(rows, headers, sourceName); toast({ title: "Downloaded", description: `${rows.length} rows exported to Excel.` }); }, onError: (err) => { toast({ title: "Error", description: err.message, variant: "destructive" }); }, }); // ── Extract & Import ───────────────────────────────────────────────────── const importMutation = useMutation({ mutationFn: async (files) => { const { rows } = await extractData(files); if (rows.length === 0) throw new Error("No rows extracted from the PDF."); const res = await apiRequest("POST", "/api/payment-pdf/import", { rows }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err?.error || "Import failed"); } return res.json(); }, onSuccess: async (data) => { toast({ title: "Imported", description: `${data.paymentIds?.length ?? 0} payment(s) created successfully.`, }); await queryClient.invalidateQueries({ queryKey: QK_PAYMENTS_RECENT_BASE }); }, onError: (err) => { toast({ title: "Error", description: err.message, variant: "destructive" }); }, }); const handleZoneFilesChange = React.useCallback((files) => { setFilesForUI(files); setError(null); }, []); const removeUploadedFile = React.useCallback((index) => { uploadZoneRef.current?.removeFile(index); }, []); const busy = downloadMutation.isPending || importMutation.isPending; return (
Upload Payment Documents Upload up to 10 MassHealth remittance PDFs. Extract and download as Excel, or import directly into the database.
{filesForUI.length > 0 && (

Uploaded ({filesForUI.length}/{MAX_FILES})

    {filesForUI.map((file, idx) => (
  • {file.name}

    {(file.size / 1024 / 1024).toFixed(2)} MB

  • ))}
)}
{/* Extract & Download */} {/* Extract & Import */}
{error &&

{error}

}
); }