feat(popup-table) - added popup table for extracted ocr data
This commit is contained in:
@@ -27,8 +27,12 @@ export default function PaymentOCRBlock() {
|
||||
const [uploadedImages, setUploadedImages] = React.useState<File[]>([]);
|
||||
const [isDragging, setIsDragging] = React.useState(false);
|
||||
const [isExtracting, setIsExtracting] = React.useState(false);
|
||||
|
||||
// extracted rows shown only inside modal
|
||||
const [rows, setRows] = React.useState<Row[]>([]);
|
||||
const [columns, setColumns] = React.useState<ColumnDef<Row>[]>([]);
|
||||
const [modalColumns, setModalColumns] = React.useState<string[]>([]);
|
||||
const [showModal, setShowModal] = React.useState(false);
|
||||
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
//Mutation
|
||||
@@ -64,29 +68,10 @@ export default function PaymentOCRBlock() {
|
||||
}, new Set())
|
||||
);
|
||||
|
||||
setColumns(
|
||||
allKeys.map((key) => ({
|
||||
id: key, // ✅ unique identifier
|
||||
header: key,
|
||||
cell: ({ row }) => (
|
||||
<input
|
||||
className="w-full border rounded p-1"
|
||||
value={(row.original[key] as string) ?? ""}
|
||||
onChange={(e) => {
|
||||
const newData = [...rows];
|
||||
newData[row.index] = {
|
||||
...newData[row.index],
|
||||
__id: newData[row.index]!.__id,
|
||||
[key]: e.target.value,
|
||||
};
|
||||
setRows(newData);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}))
|
||||
);
|
||||
setModalColumns(allKeys);
|
||||
|
||||
setIsExtracting(false);
|
||||
setShowModal(true);
|
||||
},
|
||||
|
||||
onError: (error: any) => {
|
||||
@@ -99,13 +84,6 @@ export default function PaymentOCRBlock() {
|
||||
},
|
||||
});
|
||||
|
||||
// ---- Table instance ----
|
||||
const table = useReactTable({
|
||||
data: rows,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
// ---- handlers (all in this file) -----------------------------------------
|
||||
|
||||
const handleImageSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -143,7 +121,7 @@ export default function PaymentOCRBlock() {
|
||||
const next = prev.filter((_, i) => i !== index);
|
||||
if (next.length === 0) {
|
||||
setRows([]);
|
||||
setColumns([]);
|
||||
setModalColumns([]);
|
||||
setError(null);
|
||||
}
|
||||
return next;
|
||||
@@ -222,10 +200,12 @@ export default function PaymentOCRBlock() {
|
||||
// ✅ CLEAR UI: remove files and table rows
|
||||
setUploadedImages([]);
|
||||
setRows([]);
|
||||
setColumns([]);
|
||||
setModalColumns([]);
|
||||
setError(null);
|
||||
setIsDragging(false);
|
||||
if (fileInputRef.current) fileInputRef.current.value = "";
|
||||
|
||||
setShowModal(false);
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: "Error",
|
||||
@@ -235,19 +215,6 @@ export default function PaymentOCRBlock() {
|
||||
}
|
||||
};
|
||||
|
||||
//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 (
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
@@ -358,23 +325,127 @@ export default function PaymentOCRBlock() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Results Table */}
|
||||
|
||||
{/* show extraction error if any */}
|
||||
{error && <p className="mt-4 text-sm text-red-600">{error}</p>}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{rows.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
{/* Row/Column control buttons */}
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<OCRDetailsModal
|
||||
open={showModal}
|
||||
onClose={() => setShowModal(false)}
|
||||
onSave={handleSave}
|
||||
rows={rows}
|
||||
setRows={setRows}
|
||||
columnKeys={modalColumns}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------- Simple Modal (in-app popup) ----------------
|
||||
|
||||
export function OCRDetailsModal({
|
||||
open,
|
||||
onClose,
|
||||
onSave,
|
||||
rows,
|
||||
setRows,
|
||||
columnKeys,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSave: () => void;
|
||||
rows: Row[];
|
||||
setRows: React.Dispatch<React.SetStateAction<Row[]>>;
|
||||
columnKeys: string[];
|
||||
}) {
|
||||
if (!open) return null;
|
||||
|
||||
//rows helper
|
||||
const handleDeleteRow = (index: number) => {
|
||||
setRows((prev) => prev.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const handleAddRow = React.useCallback(() => {
|
||||
setRows((prev) => {
|
||||
const newRow: Row = { __id: prev.length };
|
||||
columnKeys.forEach((k) => {
|
||||
newRow[k] = "";
|
||||
});
|
||||
return [...prev, newRow];
|
||||
});
|
||||
}, [setRows, columnKeys]);
|
||||
|
||||
const modalColumns = React.useMemo<ColumnDef<Row>[]>(() => {
|
||||
// ensure ICN (if present) is moved to the end of the data columns
|
||||
const reorderedKeys = [
|
||||
...columnKeys.filter((k) => k !== "ICN"),
|
||||
...(columnKeys.includes("ICN") ? ["ICN"] : []),
|
||||
];
|
||||
|
||||
return reorderedKeys.map((key) => ({
|
||||
id: key,
|
||||
header: key,
|
||||
cell: ({ row }) => {
|
||||
const value = (row.original[key] ?? "") as string;
|
||||
return (
|
||||
<input
|
||||
className="w-full border rounded p-1"
|
||||
value={String(value)}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value;
|
||||
setRows((prev) => {
|
||||
const next = [...prev];
|
||||
next[row.index] = {
|
||||
...next[row.index],
|
||||
__id: next[row.index]!.__id,
|
||||
[key]: v,
|
||||
};
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}));
|
||||
}, [columnKeys, setRows]);
|
||||
|
||||
const table = useReactTable({
|
||||
data: rows,
|
||||
columns: modalColumns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-start justify-center p-6">
|
||||
<div
|
||||
className="absolute inset-0 bg-black/40"
|
||||
onClick={onClose}
|
||||
aria-hidden
|
||||
/>
|
||||
|
||||
{/* larger modal, column layout so footer sticks to bottom */}
|
||||
<div className="relative z-10 w-full max-w-[1600px] h-[92vh] bg-white rounded-lg shadow-2xl overflow-hidden flex flex-col">
|
||||
{/* header */}
|
||||
<div className="flex items-center justify-between p-4 border-b">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button size="sm" onClick={handleAddRow}>
|
||||
<Plus className="h-4 w-4 mr-1" /> Add Row
|
||||
<Plus className="h-4 w-4 mr-2" /> Add Row
|
||||
</Button>
|
||||
<h3 className="text-lg font-medium ml-2">OCR Payment Details</h3>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div>
|
||||
<Button size="sm" variant="ghost" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="border-collapse border border-gray-300 w-full table-auto min-w-max">
|
||||
{/* body (scrollable) */}
|
||||
<div className="p-4 overflow-auto flex-1">
|
||||
<div className="min-w-max">
|
||||
<table className="border-collapse border border-gray-300 w-full">
|
||||
<thead>
|
||||
{table.getHeaderGroups().map((hg) => (
|
||||
<tr key={hg.id} className="bg-gray-100">
|
||||
@@ -393,39 +464,26 @@ export default function PaymentOCRBlock() {
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{table.getRowModel().rows.map((row, rowIndex) => (
|
||||
<tr key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
const colId = cell.column.id; // ✅ key for field
|
||||
return (
|
||||
{table.getRowModel().rows.map((r) => (
|
||||
<tr key={r.id}>
|
||||
{r.getVisibleCells().map((cell) => (
|
||||
<td
|
||||
key={cell.id}
|
||||
className="border p-2 whitespace-nowrap"
|
||||
>
|
||||
<input
|
||||
className="w-full border rounded p-1"
|
||||
value={
|
||||
(rows[rowIndex]?.[colId] as string) ?? ""
|
||||
}
|
||||
onChange={(e) => {
|
||||
const newData = [...rows];
|
||||
newData[rowIndex] = {
|
||||
...newData[rowIndex],
|
||||
__id: newData[rowIndex]!.__id, // keep id
|
||||
[colId]: e.target.value,
|
||||
};
|
||||
setRows(newData);
|
||||
}}
|
||||
/>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
<td className="border p-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={() => handleDeleteRow(rowIndex)}
|
||||
onClick={() => handleDeleteRow(r.index)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
@@ -435,19 +493,15 @@ export default function PaymentOCRBlock() {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="w-full h-12 gap-2"
|
||||
type="button"
|
||||
variant="warning"
|
||||
onClick={handleSave}
|
||||
>
|
||||
{/* footer (always visible) */}
|
||||
<div className="p-4 border-t flex justify-end">
|
||||
<Button type="button" className="h-12" onClick={onSave}>
|
||||
Save Edited Data
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user