"use client"; import { useState } from "react"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { flexRender, getCoreRowModel, useReactTable, ColumnDef, } from "@tanstack/react-table"; import { Button } from "@/components/ui/button"; import { Plus, Pencil, Trash2, Loader2 } from "lucide-react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from "@/components/ui/dialog"; import { useForm } from "react-hook-form"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; interface Field { name: string; label: string; type: "text" | "number" | "checkbox" | "select" | "textarea"; required?: boolean; options?: { label: string; value: string | number }[]; } interface GenericCrudTableProps { title: string; description?: string; entityName: string; queryKey: any[]; fetchFn: () => Promise; createFn: (data: any) => Promise; updateFn: (id: number, data: any) => Promise; deleteFn: (id: number) => Promise; columns: ColumnDef[]; fields: Field[]; filters?: React.ReactNode; } export function GenericCrudTable({ title, description, entityName, queryKey, fetchFn, createFn, updateFn, deleteFn, columns, fields, filters, }: GenericCrudTableProps) { const queryClient = useQueryClient(); const [isDialogOpen, setIsDialogOpen] = useState(false); const [editingItem, setEditingId] = useState(null); const [itemToDelete, setItemToDelete] = useState(null); const { data: rawData, isLoading, refetch } = useQuery({ queryKey, queryFn: fetchFn, }); // ADR-019: Support both direct array or wrapped data object const data: T[] = Array.isArray(rawData) ? rawData : (rawData as any)?.data || []; const createMutation = useMutation({ mutationFn: createFn, onSuccess: () => { queryClient.invalidateQueries({ queryKey }); toast.success(`${entityName} created successfully`); setIsDialogOpen(false); reset(); }, onError: (error: any) => { toast.error(error.response?.data?.message || `Failed to create ${entityName}`); }, }); const updateMutation = useMutation({ mutationFn: ({ id, data }: { id: number; data: any }) => updateFn(id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey }); toast.success(`${entityName} updated successfully`); setIsDialogOpen(false); setEditingId(null); reset(); }, onError: (error: any) => { toast.error(error.response?.data?.message || `Failed to update ${entityName}`); }, }); const deleteMutation = useMutation({ mutationFn: deleteFn, onSuccess: () => { queryClient.invalidateQueries({ queryKey }); toast.success(`${entityName} deleted successfully`); setItemToDelete(null); }, onError: (error: any) => { toast.error(error.response?.data?.message || `Failed to delete ${entityName}`); }, }); const { register, handleSubmit, reset, setValue, watch, formState: { errors }, } = useForm(); const table = useReactTable({ data, columns: [ ...columns, { id: "actions", cell: ({ row }) => (
), }, ], getCoreRowModel: getCoreRowModel(), }); const handleAdd = () => { setEditingId(null); reset(); fields.forEach((f) => { if (f.type === "checkbox") setValue(f.name, true); }); setIsDialogOpen(true); }; const handleEdit = (item: any) => { setEditingId(item.id); reset(item); // Ensure select values are strings for Shadcn Select fields.forEach(f => { if (f.type === 'select' && item[f.name]) { setValue(f.name, String(item[f.name])); } }); setIsDialogOpen(true); }; const onSubmit = (formData: any) => { if (editingItem) { updateMutation.mutate({ id: editingItem, data: formData }); } else { createMutation.mutate(formData); } }; return (

{title}

{description && (

{description}

)}
{filters &&
{filters}
}
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( {header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext() )} ))} ))} {isLoading ? (
Loading...
) : data.length === 0 ? ( No data found. ) : ( table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => ( {flexRender( cell.column.columnDef.cell, cell.getContext() )} ))} )) )}
{editingItem ? `Edit ${entityName}` : `Add New ${entityName}`}
{fields.map((field) => (
{field.type === "checkbox" ? (
setValue(field.name, checked)} />
) : field.type === "select" ? ( ) : field.type === "textarea" ? (