import * as React from 'react'; import { alpha } from '@mui/material/styles'; import { Box, Typography, Button, IconButton, Tooltip, Card, CardContent, CardActions, Grid, Menu, MenuItem, useMediaQuery, useTheme, Divider, Chip, Stack, } from '@mui/material'; import { DataGrid, GridColDef, GridActionsCellItem, GridRenderCellParams, GridPaginationModel, } from '@mui/x-data-grid'; import EditIcon from '@mui/icons-material/Edit'; import DeleteIcon from '@mui/icons-material/Delete'; import VisibilityIcon from '@mui/icons-material/Visibility'; import MoreVertIcon from '@mui/icons-material/MoreVert'; import { useNavigate } from 'react-router-dom'; import { ResourceConfig } from '../types/config'; interface EnhancedTableProps { config: ResourceConfig; data: any[]; total?: number; paginationModel?: GridPaginationModel; onPaginationModelChange?: (model: GridPaginationModel) => void; loading?: boolean; onEdit: (item: any) => void; onDelete: (id: string) => void; onCreate: () => void; onNavigateToResource?: (resourceName: string, id: string) => void; } export default function EnhancedTable({ config, data, total, paginationModel, onPaginationModelChange, loading = false, onEdit, onDelete, onCreate, onNavigateToResource, }: EnhancedTableProps) { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('md')); const navigate = useNavigate(); const columns: GridColDef[] = React.useMemo(() => { const cols: GridColDef[] = Object.entries(config.fields).map(([key, field]) => { let muiType: 'string' | 'number' | 'boolean' | 'date' | 'dateTime' | 'singleSelect' = 'string'; if (field.type === 'number') muiType = 'number'; if (field.type === 'boolean') muiType = 'boolean'; if (field.type === 'date') muiType = 'date'; if (field.type === 'datetime') muiType = 'dateTime'; if (field.type === 'enum') muiType = 'singleSelect'; const col: GridColDef = { field: key, headerName: field.label, type: muiType, flex: 1, minWidth: 150, renderCell: (params: GridRenderCellParams) => }; if (muiType === 'date' || muiType === 'dateTime') { col.valueGetter = (value: any) => { if (!value) return null; const date = new Date(value); return isNaN(date.getTime()) ? null : date; }; } if (muiType === 'singleSelect' && field.options) { // @ts-ignore col.valueOptions = field.options; } return col; }); cols.push({ field: 'actions', type: 'actions', headerName: 'Actions', width: 120, getActions: (params) => [ } label="View" onClick={() => navigate(`/admin/${config.name}/${params.id}`)} />, } label="Edit" onClick={() => navigate(`/admin/${config.name}/edit/${params.id}`)} />, } label="Delete" onClick={() => onDelete(params.id as string)} />, ], }); return cols; }, [config, onDelete, navigate, onNavigateToResource]); if (isMobile) { return ( {config.pluralLabel} {data.map((row) => ( ))} ); } return ( {config.pluralLabel} { if (!config.pagination) return data.length; if (total !== undefined) return total; // Graceful fallback for missing total count const page = paginationModel?.page || 0; const pageSize = paginationModel?.pageSize || 10; if (data.length < pageSize) { return page * pageSize + data.length; } // Enable 'Next' button by pretending there's at least one more page return (page + 2) * pageSize; })()} loading={loading} paginationModel={paginationModel || { page: 0, pageSize: 10 }} onPaginationModelChange={onPaginationModelChange} getRowId={(row) => { const pk = config.primaryKey; if (row[pk] !== undefined && row[pk] !== null) return row[pk]; const fallbackKeys = ['id', '_id', 'uuid', 'pk']; for (const key of fallbackKeys) { if (row[key] !== undefined && row[key] !== null) return row[key]; } return `temp-id-${data.indexOf(row)}`; }} disableRowSelectionOnClick pageSizeOptions={[10, 25, 50]} sx={{ border: 'none', '& .MuiDataGrid-cell:focus': { outline: 'none' }, '& .MuiDataGrid-columnHeader:focus': { outline: 'none' }, }} /> ); } function MobileCardRow({ row, config, onDelete, onNavigate, navigate }: any) { const [anchorEl, setAnchorEl] = React.useState(null); const open = Boolean(anchorEl); const id = row[config.primaryKey]; const handleClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); }; const handleClose = () => { setAnchorEl(null); }; return ( #{id} { handleClose(); navigate(`/admin/${config.name}/${id}`); }}>View { handleClose(); navigate(`/admin/${config.name}/edit/${id}`); }}>Edit { handleClose(); onDelete(id); }} sx={{ color: 'error.main' }}>Delete {Object.entries(config.fields).slice(0, 5).map(([key, field]: [string, any]) => ( {field.label} ))} ); } function getFormattedDisplayValue(item: any, displayField?: string | string[]) { if (!item) return ""; if (!displayField) return item.name || item.title || item.label || item.id || JSON.stringify(item); if (Array.isArray(displayField)) { return displayField .map(key => item[key]) .filter(val => val !== undefined && val !== null) .join(' '); } return item[displayField] || item.id || JSON.stringify(item); } function FieldRenderer({ params, field, fieldKey, config, onNavigate, navigate, isMobile }: any) { const value = params.value; const isPk = fieldKey === config.primaryKey; if (field.formatter) return field.formatter(value); // 1. Single Relation if (field.relation && value && !Array.isArray(value)) { const relationId = typeof value === 'object' ? (value.id || value._id || value.pk) : value; const displayValue = getFormattedDisplayValue(value, field.displayField); return ( { e.stopPropagation(); if (relationId) onNavigate?.(field.relation!, String(relationId)); }} sx={{ cursor: 'pointer' }} /> ); } // 2. Multi-Select (Array of relations or simple strings) if (field.type === 'array' && Array.isArray(value)) { const tooltipTitle = value.map((item) => getFormattedDisplayValue(item, field.displayField)).join(', '); return ( {value.map((item, idx) => ( { e.stopPropagation(); if (field.relation) { const id = typeof item === 'object' ? (item.id || item._id) : item; if (id) onNavigate?.(field.relation!, String(id)); } }} /> ))} ); } // 3. Simple Objects if (field.type === 'object' && value) { return getFormattedDisplayValue(value, field.displayField) || (isMobile ? 'Object' : JSON.stringify(value)); } if (field.type === 'number' && typeof value === 'number') { const isNegative = value < 0; const color = isNegative ? 'error' : 'success'; return ( alpha(theme.palette[color].main, 0.15), color: (theme) => theme.palette[color].dark, '& .MuiChip-label': { px: 1.5 } }} /> ); } if (field.type === 'boolean') { return value ? ( ) : ( ); } if (field.type === 'datetime' || field.type === 'date') return value ? new Date(value).toLocaleString() : ''; if (isPk && !isMobile) { return ( { e.stopPropagation(); navigate(`/admin/${config.name}/${params.row[config.primaryKey]}`); }} sx={{ cursor: 'pointer', fontWeight: 'bold' }} /> ); } return value; }