feat(ocr paymentpage) - frontend table added
This commit is contained in:
@@ -12,8 +12,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^3.10.0",
|
"@hookform/resolvers": "^3.10.0",
|
||||||
"@jridgewell/trace-mapping": "^0.3.25",
|
"@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-accordion": "^1.2.4",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.7",
|
"@radix-ui/react-alert-dialog": "^1.1.7",
|
||||||
"@radix-ui/react-aspect-ratio": "^1.1.3",
|
"@radix-ui/react-aspect-ratio": "^1.1.3",
|
||||||
@@ -41,6 +39,8 @@
|
|||||||
"@radix-ui/react-toggle": "^1.1.3",
|
"@radix-ui/react-toggle": "^1.1.3",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.3",
|
"@radix-ui/react-toggle-group": "^1.1.3",
|
||||||
"@radix-ui/react-tooltip": "^1.2.0",
|
"@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",
|
"@replit/vite-plugin-shadcn-theme-json": "^0.0.4",
|
||||||
"@repo/db": "*",
|
"@repo/db": "*",
|
||||||
"@repo/typescript-config": "*",
|
"@repo/typescript-config": "*",
|
||||||
|
|||||||
@@ -1,24 +1,31 @@
|
|||||||
// PaymentOCRBlock.tsx
|
// PaymentOCRBlock.tsx
|
||||||
import * as React from "react";
|
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 { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
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 { useMutation } from "@tanstack/react-query";
|
||||||
import { apiRequest } from "@/lib/queryClient";
|
import { apiRequest } from "@/lib/queryClient";
|
||||||
import { toast } from "@/hooks/use-toast";
|
import { toast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
type Row = Record<string, string | number | boolean | null | undefined>;
|
import {
|
||||||
|
useReactTable,
|
||||||
|
getCoreRowModel,
|
||||||
|
flexRender,
|
||||||
|
ColumnDef,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
|
|
||||||
|
// ---------------- Types ----------------
|
||||||
|
|
||||||
|
type Row = { __id: number } & Record<string, string | number | null>;
|
||||||
|
|
||||||
export default function PaymentOCRBlock() {
|
export default function PaymentOCRBlock() {
|
||||||
// UI state
|
// UI state
|
||||||
const [uploadedImage, setUploadedImage] = React.useState<File | null>(null);
|
|
||||||
const [uploadedImages, setUploadedImages] = React.useState<File[]>([]);
|
const [uploadedImages, setUploadedImages] = React.useState<File[]>([]);
|
||||||
const [isDragging, setIsDragging] = React.useState(false);
|
const [isDragging, setIsDragging] = React.useState(false);
|
||||||
const [isExtracting, setIsExtracting] = React.useState(false);
|
const [isExtracting, setIsExtracting] = React.useState(false);
|
||||||
const [rows, setRows] = React.useState<Row[]>([]);
|
const [rows, setRows] = React.useState<Row[]>([]);
|
||||||
|
const [columns, setColumns] = React.useState<ColumnDef<Row>[]>([]);
|
||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
|
|
||||||
//Mutation
|
//Mutation
|
||||||
@@ -37,24 +44,68 @@ export default function PaymentOCRBlock() {
|
|||||||
const data = (await res.json()) as { rows: Row[] } | Row[];
|
const data = (await res.json()) as { rows: Row[] } | Row[];
|
||||||
return Array.isArray(data) ? data : data.rows;
|
return Array.isArray(data) ? data : data.rows;
|
||||||
},
|
},
|
||||||
onSuccess: (rows) => {
|
onSuccess: (data) => {
|
||||||
setRows(rows);
|
const withIds: Row[] = data.map((r, i) => ({ ...r, __id: i }));
|
||||||
|
setRows(withIds);
|
||||||
|
|
||||||
|
const allKeys = Array.from(
|
||||||
|
data.reduce<Set<string>>((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 }) => (
|
||||||
|
<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);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
setIsExtracting(false);
|
||||||
},
|
},
|
||||||
|
|
||||||
onError: (error: any) => {
|
onError: (error: any) => {
|
||||||
toast({
|
toast({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
description: `Failed to extract payment data: ${error.message}`,
|
description: `Failed to extract payment data: ${error.message}`,
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
|
setIsExtracting(false);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- Table instance ----
|
||||||
|
const table = useReactTable({
|
||||||
|
data: rows,
|
||||||
|
columns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
});
|
||||||
|
|
||||||
// ---- handlers (all in this file) -----------------------------------------
|
// ---- handlers (all in this file) -----------------------------------------
|
||||||
|
|
||||||
const handleImageSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleImageSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const files = e.target.files;
|
const list = Array.from(e.target.files || []);
|
||||||
if (!files || !files.length) return;
|
if (!list.length) return;
|
||||||
const list = Array.from(files);
|
if (list.length > 10) {
|
||||||
setUploadedImage(list[0] as File); // preview first
|
setError("You can only upload up to 10 images.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
setUploadedImages(list);
|
setUploadedImages(list);
|
||||||
setError(null);
|
setError(null);
|
||||||
};
|
};
|
||||||
@@ -69,25 +120,56 @@ export default function PaymentOCRBlock() {
|
|||||||
setError("Please drop image files (JPG/PNG/TIFF/BMP).");
|
setError("Please drop image files (JPG/PNG/TIFF/BMP).");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setUploadedImage(list[0] as File);
|
if (list.length > 10) {
|
||||||
|
setError("You can only upload up to 10 images.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setUploadedImages(list);
|
setUploadedImages(list);
|
||||||
setError(null);
|
setError(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeUploadedImage = () => {
|
const removeUploadedImage = (index: number) => {
|
||||||
setUploadedImage(null);
|
setUploadedImages((prev) => {
|
||||||
setUploadedImages([]);
|
const next = prev.filter((_, i) => i !== index);
|
||||||
|
if (next.length === 0) {
|
||||||
setRows([]);
|
setRows([]);
|
||||||
|
setColumns([]);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleExtract = () => {
|
const handleExtract = () => {
|
||||||
if (!uploadedImages.length) return;
|
if (!uploadedImages.length) return;
|
||||||
|
setIsExtracting(true);
|
||||||
extractPaymentOCR.mutate(uploadedImages);
|
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 (
|
return (
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
@@ -97,13 +179,13 @@ export default function PaymentOCRBlock() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6 space-y-6">
|
||||||
<div className="flex gap-4">
|
{/* Upload block */}
|
||||||
<div
|
<div
|
||||||
className={`flex-1 border-2 border-dashed rounded-lg p-8 text-center transition-colors ${
|
className={`flex-1 border-2 border-dashed rounded-lg p-8 text-center transition-colors ${
|
||||||
isDragging
|
isDragging
|
||||||
? "border-blue-400 bg-blue-50"
|
? "border-blue-400 bg-blue-50"
|
||||||
: uploadedImage
|
: uploadedImages.length
|
||||||
? "border-green-400 bg-green-50"
|
? "border-green-400 bg-green-50"
|
||||||
: "border-gray-300 bg-gray-50 hover:border-gray-400"
|
: "border-gray-300 bg-gray-50 hover:border-gray-400"
|
||||||
}`}
|
}`}
|
||||||
@@ -117,48 +199,49 @@ export default function PaymentOCRBlock() {
|
|||||||
document.getElementById("image-upload-input")?.click()
|
document.getElementById("image-upload-input")?.click()
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{uploadedImage ? (
|
{uploadedImages.length ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-center space-x-4">
|
{uploadedImages.map((file, idx) => (
|
||||||
<ImageIcon className="h-8 w-8 text-green-500" />
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="flex items-center justify-between space-x-4 border rounded-md p-2 bg-white"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<ImageIcon className="h-6 w-6 text-green-500" />
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<p className="font-medium text-green-700">
|
<p className="font-medium text-green-700">
|
||||||
{uploadedImage.name}
|
{file.name}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
{(uploadedImage.size / 1024 / 1024).toFixed(2)} MB
|
{(file.size / 1024 / 1024).toFixed(2)} MB
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
removeUploadedImage();
|
removeUploadedImage(idx);
|
||||||
}}
|
}}
|
||||||
className="ml-auto"
|
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{isExtracting && (
|
))}
|
||||||
<div className="text-sm text-blue-600">
|
|
||||||
Extracting payment information...
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Upload className="h-12 w-12 text-gray-400 mx-auto" />
|
<Upload className="h-12 w-12 text-gray-400 mx-auto" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-lg font-medium text-gray-700 mb-2">
|
<p className="text-lg font-medium text-gray-700 mb-2">
|
||||||
Upload Payment Document
|
Upload Payment Documents
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-500 mb-4">
|
<p className="text-sm text-gray-500 mb-4">
|
||||||
Drag and drop image(s) or click to browse
|
Drag and drop up to 10 images or click to browse
|
||||||
</p>
|
</p>
|
||||||
<Button variant="outline" type="button">
|
<Button variant="outline" type="button">
|
||||||
Choose Image
|
Choose Images
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-400">
|
<p className="text-xs text-gray-400">
|
||||||
@@ -166,7 +249,6 @@ export default function PaymentOCRBlock() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<input
|
<input
|
||||||
id="image-upload-input"
|
id="image-upload-input"
|
||||||
type="file"
|
type="file"
|
||||||
@@ -177,8 +259,11 @@ export default function PaymentOCRBlock() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col justify-center">
|
{/* Extract */}
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-4">
|
||||||
<Button
|
<Button
|
||||||
|
className="w-full h-12 gap-2"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleExtract}
|
onClick={handleExtract}
|
||||||
disabled={!uploadedImages.length || isExtracting}
|
disabled={!uploadedImages.length || isExtracting}
|
||||||
@@ -189,88 +274,221 @@ export default function PaymentOCRBlock() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Results */}
|
{/* Results Table */}
|
||||||
|
|
||||||
{error && <p className="mt-4 text-sm text-red-600">{error}</p>}
|
{error && <p className="mt-4 text-sm text-red-600">{error}</p>}
|
||||||
<OCRTable rows={rows} />
|
|
||||||
|
{rows.length > 0 && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Row/Column control buttons */}
|
||||||
|
<div className="flex gap-2 flex-wrap">
|
||||||
|
<Button size="sm" onClick={handleAddRow}>
|
||||||
|
<Plus className="h-4 w-4 mr-1" /> Add Row
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="border-collapse border border-gray-300 w-full table-auto min-w-max">
|
||||||
|
<thead>
|
||||||
|
{table.getHeaderGroups().map((hg) => (
|
||||||
|
<tr key={hg.id} className="bg-gray-100">
|
||||||
|
{hg.headers.map((header) => (
|
||||||
|
<th
|
||||||
|
key={header.id}
|
||||||
|
className="border p-2 text-left whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext()
|
||||||
|
)}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
<th className="border p-2">Actions</th>
|
||||||
|
</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 (
|
||||||
|
<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);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<td className="border p-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => handleDeleteRow(rowIndex)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="w-full h-12 gap-2"
|
||||||
|
type="button"
|
||||||
|
variant="warning"
|
||||||
|
onClick={handleSave}
|
||||||
|
>
|
||||||
|
Save Edited Data
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------- helpers (kept in same file) ----------------------------
|
// --------------------- Editable OCRTable ----------------------------
|
||||||
|
|
||||||
function OCRTable({ rows }: { rows: Row[] }) {
|
// function OCRTable({
|
||||||
if (!rows?.length) return null;
|
// rows,
|
||||||
|
// setRows,
|
||||||
|
// }: {
|
||||||
|
// rows: Row[];
|
||||||
|
// setRows: React.Dispatch<React.SetStateAction<Row[]>>;
|
||||||
|
// }) {
|
||||||
|
// const [columns, setColumns] = React.useState<string[]>([]);
|
||||||
|
|
||||||
// prefer Excel-like order if present, then append any extra columns
|
// // Initialize columns once when rows come in
|
||||||
const desired = [
|
// React.useEffect(() => {
|
||||||
"Patient Name",
|
// if (rows.length && columns.length === 0) {
|
||||||
"Patient ID",
|
// const dynamicCols = Array.from(
|
||||||
"ICN",
|
// rows.reduce<Set<string>>((acc, r) => {
|
||||||
"CDT Code",
|
// Object.keys(r || {}).forEach((k) => acc.add(k));
|
||||||
"Tooth",
|
// return acc;
|
||||||
"Date SVC",
|
// }, new Set())
|
||||||
"Billed Amount",
|
// );
|
||||||
"Allowed Amount",
|
// setColumns(dynamicCols);
|
||||||
"Paid Amount",
|
// }
|
||||||
"Extraction Success",
|
// }, [rows]);
|
||||||
"Source File",
|
|
||||||
];
|
|
||||||
const dynamicCols = Array.from(
|
|
||||||
rows.reduce<Set<string>>((acc, r) => {
|
|
||||||
Object.keys(r || {}).forEach((k) => acc.add(k));
|
|
||||||
return acc;
|
|
||||||
}, new Set())
|
|
||||||
);
|
|
||||||
|
|
||||||
const columns = [
|
// if (!rows?.length) return null;
|
||||||
...desired.filter((c) => dynamicCols.includes(c)),
|
|
||||||
...dynamicCols.filter((c) => !desired.includes(c)),
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
// // ---------------- column handling ----------------
|
||||||
<div className="mt-6 overflow-auto border rounded-lg">
|
// const handleColumnNameChange = (colIdx: number, value: string) => {
|
||||||
<table className="min-w-full text-sm">
|
// // Just update local columns state for typing responsiveness
|
||||||
<thead className="bg-gray-50">
|
// setColumns((prev) => prev.map((c, i) => (i === colIdx ? value : c)));
|
||||||
<tr>
|
// };
|
||||||
{columns.map((c) => (
|
|
||||||
<th
|
|
||||||
key={c}
|
|
||||||
className="px-3 py-2 text-left font-semibold text-gray-700 whitespace-nowrap"
|
|
||||||
>
|
|
||||||
{c}
|
|
||||||
</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{rows.map((r, i) => (
|
|
||||||
<tr key={i} className="odd:bg-white even:bg-gray-50">
|
|
||||||
{columns.map((c) => (
|
|
||||||
<td key={c} className="px-3 py-2 whitespace-nowrap">
|
|
||||||
{formatCell(r[c])}
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatCell(v: unknown) {
|
// const commitColumnRename = (colIdx: number) => {
|
||||||
if (v === null || v === undefined) return "";
|
// const oldName = Object.keys(rows[0] ?? {})[colIdx];
|
||||||
if (typeof v === "boolean") return v ? "Yes" : "No";
|
// const newName = columns[colIdx];
|
||||||
return String(v);
|
// if (!oldName || !newName || oldName === newName) return;
|
||||||
}
|
|
||||||
|
|
||||||
async function safeJson(resp: Response) {
|
// const updated = rows.map((r) => {
|
||||||
try {
|
// const { [oldName]: oldVal, ...rest } = r;
|
||||||
return await resp.json();
|
// return { ...rest, [newName]: oldVal };
|
||||||
} catch {
|
// });
|
||||||
return null;
|
// 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 (
|
||||||
|
// <div className="mt-6 overflow-auto border rounded-lg">
|
||||||
|
// <table className="min-w-full text-sm">
|
||||||
|
// <thead className="bg-gray-50">
|
||||||
|
// <tr>
|
||||||
|
// {columns.map((c, idx) => (
|
||||||
|
// <th
|
||||||
|
// key={idx}
|
||||||
|
// className="px-3 py-2 text-left font-semibold text-gray-700 whitespace-nowrap"
|
||||||
|
// >
|
||||||
|
// <input
|
||||||
|
// type="text"
|
||||||
|
// className="border rounded px-2 py-1 text-sm font-semibold w-40"
|
||||||
|
// value={c}
|
||||||
|
// onChange={(e) => handleColumnNameChange(idx, e.target.value)}
|
||||||
|
// onBlur={() => commitColumnRename(idx)}
|
||||||
|
// onKeyDown={(e) => {
|
||||||
|
// if (e.key === "Enter") {
|
||||||
|
// e.currentTarget.blur(); // commit rename
|
||||||
|
// }
|
||||||
|
// }}
|
||||||
|
// />
|
||||||
|
// </th>
|
||||||
|
// ))}
|
||||||
|
// <th className="px-3 py-2">
|
||||||
|
// <Button size="sm" variant="outline" onClick={addColumn}>
|
||||||
|
// + Add Column
|
||||||
|
// </Button>
|
||||||
|
// </th>
|
||||||
|
// </tr>
|
||||||
|
// </thead>
|
||||||
|
// <tbody>
|
||||||
|
// {rows.map((r, i) => (
|
||||||
|
// <tr key={i} className="odd:bg-white even:bg-gray-50">
|
||||||
|
// {columns.map((c) => (
|
||||||
|
// <td key={c} className="px-3 py-2 whitespace-nowrap">
|
||||||
|
// <input
|
||||||
|
// type="text"
|
||||||
|
// className="w-full border rounded px-2 py-1 text-sm"
|
||||||
|
// value={r[c] != null ? String(r[c]) : ""}
|
||||||
|
// onChange={(e) => handleCellChange(i, c, e.target.value)}
|
||||||
|
// />
|
||||||
|
// </td>
|
||||||
|
// ))}
|
||||||
|
// </tr>
|
||||||
|
// ))}
|
||||||
|
// <tr>
|
||||||
|
// <td colSpan={columns.length + 1} className="px-3 py-2 text-center">
|
||||||
|
// <Button size="sm" variant="outline" onClick={addRow}>
|
||||||
|
// + Add Row
|
||||||
|
// </Button>
|
||||||
|
// </td>
|
||||||
|
// </tr>
|
||||||
|
// </tbody>
|
||||||
|
// </table>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|||||||
Reference in New Issue
Block a user