import React, { useState, useMemo, useRef, useEffect } from 'react'; import { useLanguage } from '../../contexts/LanguageContext'; import styles from './FormGenerator.module.css'; // Types for the FormGenerator export interface ColumnConfig { key: string; label: string; type?: 'string' | 'number' | 'date' | 'boolean' | 'enum'; width?: number; minWidth?: number; maxWidth?: number; sortable?: boolean; filterable?: boolean; searchable?: boolean; formatter?: (value: any, row: any) => React.ReactNode; filterOptions?: string[]; // For enum/select filters } export interface FormGeneratorProps { data: T[]; columns?: ColumnConfig[]; title?: string; searchable?: boolean; filterable?: boolean; sortable?: boolean; resizable?: boolean; pagination?: boolean; pageSize?: number; pageSizeOptions?: number[]; showPageSizeSelector?: boolean; onRowClick?: (row: T, index: number) => void; onRowSelect?: (selectedRows: T[]) => void; selectable?: boolean; loading?: boolean; actions?: { label: string; onClick: (row: T) => void; icon?: string | React.ReactNode | ((row: T) => React.ReactNode); }[]; onDelete?: (row: T) => void; onDeleteMultiple?: (rows: T[]) => void; className?: string; } export function FormGenerator>({ data, columns: providedColumns, searchable = true, filterable = true, sortable = true, resizable = true, pagination = true, pageSize = 10, pageSizeOptions = [10, 25, 50, 100], showPageSizeSelector = true, onRowClick, onRowSelect, selectable = true, // Default to true for selection functionality loading = false, actions = [], onDelete, onDeleteMultiple, className = '' }: FormGeneratorProps) { const { t } = useLanguage(); // Auto-detect columns if not provided const detectedColumns = useMemo((): ColumnConfig[] => { if (providedColumns) return providedColumns; if (data.length === 0) return []; const sampleRow = data[0]; return Object.keys(sampleRow).map(key => { const value = sampleRow[key]; let type: ColumnConfig['type'] = 'string'; // Auto-detect type based on value if (typeof value === 'number') { type = 'number'; } else if (typeof value === 'boolean') { type = 'boolean'; } else if (value instanceof Date || (typeof value === 'string' && !isNaN(Date.parse(value)) && value.includes('-'))) { type = 'date'; } return { key, label: key.charAt(0).toUpperCase() + key.slice(1).replace(/([A-Z])/g, ' $1'), type, sortable: true, filterable: true, searchable: type === 'string', width: 150, minWidth: 100, maxWidth: 400 }; }); }, [data, providedColumns]); // State management const [searchTerm, setSearchTerm] = useState(''); const [searchFocused, setSearchFocused] = useState(false); const [filterFocused, setFilterFocused] = useState>({}); const [sortConfig, setSortConfig] = useState<{ key: string; direction: 'asc' | 'desc' } | null>(null); const [filters, setFilters] = useState>({}); const [columnWidths, setColumnWidths] = useState>({}); const [selectedRows, setSelectedRows] = useState>(new Set()); const [currentPage, setCurrentPage] = useState(1); const [currentPageSize, setCurrentPageSize] = useState(pageSize); // Refs for resizing const tableRef = useRef(null); const resizingColumn = useRef(null); const startX = useRef(0); const startWidth = useRef(0); // Initialize column widths useEffect(() => { const initialWidths: Record = {}; detectedColumns.forEach(col => { // Set a default width if none specified to ensure all columns have explicit widths initialWidths[col.key] = col.width || 150; }); setColumnWidths(initialWidths); }, [detectedColumns]); // Filter and search data const filteredData = useMemo(() => { let result = [...data]; // Apply search filter if (searchTerm && searchable) { const searchLower = searchTerm.toLowerCase(); result = result.filter(row => { return detectedColumns.some(col => { if (!col.searchable) return false; const value = row[col.key]; return String(value).toLowerCase().includes(searchLower); }); }); } // Apply column filters Object.entries(filters).forEach(([key, filterValue]) => { if (filterValue !== undefined && filterValue !== '') { result = result.filter(row => { const value = row[key]; const column = detectedColumns.find(col => col.key === key); if (column?.type === 'boolean') { return Boolean(value) === Boolean(filterValue); } else if (column?.type === 'number') { return Number(value) === Number(filterValue); } else if (column?.type === 'date') { // Convert row value to DD.MM.YYYY format for comparison const rowDate = new Date(value); const rowFormatted = `${rowDate.getDate().toString().padStart(2, '0')}.${(rowDate.getMonth() + 1).toString().padStart(2, '0')}.${rowDate.getFullYear()}`; // Check if filter value is complete (DD.MM.YYYY) if (filterValue.length === 10 && filterValue.match(/^\d{2}\.\d{2}\.\d{4}$/)) { return rowFormatted === filterValue; } else { // Partial matching for incomplete dates return rowFormatted.startsWith(filterValue); } } else { return String(value).toLowerCase().includes(String(filterValue).toLowerCase()); } }); } }); // Apply sorting if (sortConfig) { result.sort((a, b) => { const aVal = a[sortConfig.key]; const bVal = b[sortConfig.key]; if (aVal < bVal) return sortConfig.direction === 'asc' ? -1 : 1; if (aVal > bVal) return sortConfig.direction === 'asc' ? 1 : -1; return 0; }); } return result; }, [data, searchTerm, filters, sortConfig, detectedColumns, searchable]); // Pagination const paginatedData = useMemo(() => { if (!pagination) return filteredData; const startIndex = (currentPage - 1) * currentPageSize; return filteredData.slice(startIndex, startIndex + currentPageSize); }, [filteredData, currentPage, currentPageSize, pagination]); const totalPages = Math.ceil(filteredData.length / currentPageSize); // Handle sorting const handleSort = (key: string) => { if (!sortable) return; setSortConfig(current => { if (current?.key === key) { return current.direction === 'asc' ? { key, direction: 'desc' } : null; } return { key, direction: 'asc' }; }); }; // Handle filtering const handleFilter = (key: string, value: any) => { setFilters(prev => ({ ...prev, [key]: value })); setCurrentPage(1); // Reset to first page when filtering }; // Handle filter input focus const handleFilterFocus = (key: string, focused: boolean) => { setFilterFocused(prev => ({ ...prev, [key]: focused })); }; // Handle row selection const handleRowSelect = (index: number) => { if (!selectable) return; const newSelected = new Set(selectedRows); if (newSelected.has(index)) { newSelected.delete(index); } else { newSelected.add(index); } setSelectedRows(newSelected); if (onRowSelect) { const selectedData = Array.from(newSelected).map(i => paginatedData[i]); onRowSelect(selectedData); } }; // Handle select all const handleSelectAll = () => { if (!selectable) return; if (selectedRows.size === paginatedData.length) { setSelectedRows(new Set()); onRowSelect?.([]); } else { const allIndices = new Set(paginatedData.map((_, index) => index)); setSelectedRows(allIndices); onRowSelect?.(paginatedData); } }; // Handle delete single item const handleDeleteSingle = (row: T, index: number) => { if (onDelete) { onDelete(row); // Remove from selection if it was selected if (selectedRows.has(index)) { const newSelected = new Set(selectedRows); newSelected.delete(index); setSelectedRows(newSelected); if (onRowSelect) { const selectedData = Array.from(newSelected).map(i => paginatedData[i]); onRowSelect(selectedData); } } } }; // Handle delete multiple items const handleDeleteMultiple = () => { if (onDeleteMultiple && selectedRows.size > 0) { const selectedData = Array.from(selectedRows).map(i => paginatedData[i]); onDeleteMultiple(selectedData); // Clear selection setSelectedRows(new Set()); onRowSelect?.([]); } }; // Handle page size change const handlePageSizeChange = (newPageSize: number) => { setCurrentPageSize(newPageSize); setCurrentPage(1); // Reset to first page when page size changes }; // Handle column resizing const handleMouseDown = (e: React.MouseEvent, columnKey: string) => { if (!resizable) return; e.preventDefault(); resizingColumn.current = columnKey; startX.current = e.clientX; startWidth.current = columnWidths[columnKey] || 150; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); }; const handleMouseMove = (e: MouseEvent) => { if (!resizingColumn.current) return; const diff = e.clientX - startX.current; let newWidth = Math.max(100, startWidth.current + diff); // Prevent extending beyond table container const tableContainer = tableRef.current?.parentElement; if (tableContainer) { const containerWidth = tableContainer.clientWidth; const actionsColumnWidth = actions.length > 0 ? 120 : 0; // Fixed width actions column const selectColumnWidth = selectable ? 50 : 0; // Fixed width select column const fixedWidth = actionsColumnWidth + selectColumnWidth; // Calculate total width of all OTHER data columns (excluding the one being resized) const otherDataColumnsWidth = detectedColumns.reduce((total, col) => { if (col.key !== resizingColumn.current) { return total + (columnWidths[col.key] || col.width || 150); } return total; }, 0); // Maximum allowed width for this column const maxAllowedWidth = containerWidth - fixedWidth - otherDataColumnsWidth - 40; // 40px buffer newWidth = Math.min(newWidth, Math.max(100, maxAllowedWidth)); } setColumnWidths(prev => ({ ...prev, [resizingColumn.current!]: newWidth })); }; const handleMouseUp = () => { resizingColumn.current = null; document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; // Format cell value const formatCellValue = (value: any, column: ColumnConfig, row: T) => { if (column.formatter) { return column.formatter(value, row); } if (value === null || value === undefined) { return '-'; } switch (column.type) { case 'date': return new Date(value).toLocaleDateString(); case 'boolean': return value ? '✓' : '✗'; case 'number': return typeof value === 'number' ? value.toLocaleString() : value; default: return String(value); } }; return (
{(searchable || filterable || (selectable && selectedRows.size > 0)) && (
{/* Delete Controls - Show when items are selected */} {selectable && selectedRows.size > 0 && (
{selectedRows.size === 1 && onDelete && ( )} {selectedRows.size > 1 && onDeleteMultiple && ( )}
)} {/* Search Controls - Hide when items are selected */} {searchable && selectedRows.size === 0 && (
setSearchTerm(e.target.value)} onFocus={() => setSearchFocused(true)} onBlur={() => setSearchFocused(false)} className={`${styles.searchInput} ${searchFocused || searchTerm ? styles.focused : ''}`} />
)} {filterable && (
{detectedColumns.filter(col => col.filterable).map(column => (
{column.type === 'boolean' ? (
{filters[column.key] && ( )}
) : column.filterOptions ? (
{filters[column.key] && ( )}
) : column.type === 'date' ? (
{ let value = e.target.value; const currentValue = filters[column.key] || ''; // Check if user is deleting (new value is shorter) const isDeleting = value.length < currentValue.length; if (isDeleting) { // When deleting, preserve the exact input without auto-formatting handleFilter(column.key, value); return; } // Auto-pad single digits followed by dot (e.g., "4." -> "04.") value = value.replace(/^(\d)\./, '0$1.'); value = value.replace(/\.(\d)\./, '.0$1.'); // Allow typing and format as DD.MM.YYYY const digitsOnly = value.replace(/\D/g, ''); // Remove non-digits let formatted = ''; if (digitsOnly.length >= 8) { // Full format: DDMMYYYY -> DD.MM.YYYY const day = digitsOnly.slice(0, 2); const month = digitsOnly.slice(2, 4); const year = digitsOnly.slice(4, 8); // Validate day (01-31) and month (01-12) if (parseInt(day) > 31 || parseInt(month) > 12 || parseInt(day) === 0 || parseInt(month) === 0) { return; // Don't update if invalid } formatted = `${day}.${month}.${year}`; } else if (digitsOnly.length >= 4) { // Partial format: DDMM -> DD.MM. const day = digitsOnly.slice(0, 2); const month = digitsOnly.slice(2, 4); const remaining = digitsOnly.slice(4); // Validate day (01-31) and month (01-12) if (parseInt(day) > 31 || parseInt(month) > 12 || parseInt(day) === 0 || parseInt(month) === 0) { return; // Don't update if invalid } formatted = `${day}.${month}.${remaining}`; } else if (digitsOnly.length >= 2) { // Start format: DD -> DD. const day = digitsOnly.slice(0, 2); const remaining = digitsOnly.slice(2); // Validate day (01-31) if (parseInt(day) > 31 || parseInt(day) === 0) { return; // Don't update if invalid } formatted = `${day}.${remaining}`; } else { // Just digits formatted = digitsOnly; } handleFilter(column.key, formatted); }} onFocus={() => handleFilterFocus(column.key, true)} onBlur={() => handleFilterFocus(column.key, false)} className={`${styles.filterInput} ${filterFocused[column.key] || filters[column.key] ? styles.focused : ''}`} maxLength={10} />
) : (
handleFilter(column.key, e.target.value)} onFocus={() => handleFilterFocus(column.key, true)} onBlur={() => handleFilterFocus(column.key, false)} className={`${styles.filterInput} ${filterFocused[column.key] || filters[column.key] ? styles.focused : ''}`} />
)}
))}
)}
)} {/* Table */}
{loading ? (

{t('common.loading', 'Loading...')}

) : ( {selectable && ( )} {actions.length > 0 && ( )} {detectedColumns.map(column => ( ))} {paginatedData.map((row, index) => ( onRowClick?.(row, index)} > {selectable && ( )} {actions.length > 0 && ( )} {detectedColumns.map(column => ( ))} ))}
0} onChange={handleSelectAll} title="Select all items" /> column.sortable && handleSort(column.key)} >
{column.label} {sortable && column.sortable && ( {sortConfig?.key === column.key ? ( sortConfig.direction === 'asc' ? '↑' : '↓' ) : '↕'} )}
{resizable && (
handleMouseDown(e, column.key)} /> )}
handleRowSelect(index)} onClick={(e) => e.stopPropagation()} title="Select this item" />
{actions.map((action, actionIndex) => ( ))}
{formatCellValue(row[column.key], column, row)}
)}
{/* Pagination */} {pagination && (
{showPageSizeSelector && (
)} {totalPages > 1 && ( <> {t('formgen.pagination.info') .replace('{page}', currentPage.toString()) .replace('{total}', totalPages.toString()) .replace('{count}', filteredData.length.toString())} )}
)}
); } export default FormGenerator;