'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 ApiError extends Error { response?: { data?: { message?: string } }; } interface GenericCrudTableProps { title: string; description?: string; entityName: string; queryKey: string[]; fetchFn: () => Promise; createFn: (data: Record) => Promise; updateFn: (id: number, data: Record) => 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, } = useQuery({ queryKey, queryFn: fetchFn, }); // ADR-019: Support both direct array or wrapped data object const data: T[] = Array.isArray(rawData) ? rawData : (rawData as { data?: T[] } | undefined)?.data || []; const createMutation = useMutation({ mutationFn: createFn, onSuccess: () => { queryClient.invalidateQueries({ queryKey }); toast.success(`${entityName} created successfully`); setIsDialogOpen(false); reset(); }, onError: (error: ApiError) => { toast.error(error.response?.data?.message || `Failed to create ${entityName}`); }, }); const updateMutation = useMutation({ mutationFn: ({ id, data }: { id: number; data: Record }) => updateFn(id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey }); toast.success(`${entityName} updated successfully`); setIsDialogOpen(false); setEditingId(null); reset(); }, onError: (error: ApiError) => { 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: ApiError) => { 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: T) => { setEditingId(item.id as number); reset(item as Record); // Ensure select values are strings for Shadcn Select fields.forEach((f) => { const record = item as Record; if (f.type === 'select' && record[f.name]) { setValue(f.name, String(record[f.name])); } }); setIsDialogOpen(true); }; const onSubmit = (formData: Record) => { 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' ? (