import React, { useState, useMemo } from 'react'; import { EntityMetadata, FieldDefinition } from '../../entities/types'; import { ViewDefinition } from '../../services/contracts/metadata'; import { RecordEntity } from '../../services/storage/indexedDb'; import { Button } from '../../shared/ui/Button'; import { Input } from '../../shared/ui/Input'; import { Badge } from '../../shared/ui/Badge'; import { formatFieldValue } from '../../shared/lib/fieldEngine'; import { Search, ArrowUpDown, ArrowUp, ArrowDown, Eye, Edit2, Trash2, Plus, Inbox, } from 'lucide-react'; export interface DynamicTableProps { entity: EntityMetadata; records: RecordEntity[]; view?: ViewDefinition | null; isLoading?: boolean; onRowClick?: (record: RecordEntity) => void; onEdit?: (record: RecordEntity) => void; onDelete?: (record: RecordEntity) => void; onCreateNew?: () => void; } export const DynamicTable: React.FC = ({ entity, records, view, isLoading = false, onRowClick, onEdit, onDelete, onCreateNew, }) => { const [searchQuery, setSearchQuery] = useState(''); const [sortField, setSortField] = useState(view?.sortBy || 'updatedAt'); const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>(view?.sortOrder || 'desc'); const [currentPage, setCurrentPage] = useState(1); const pageSize = 15; const fieldKeyMap = useMemo(() => { const map = new Map(); entity.fields.forEach((f) => map.set(f.key, f)); return map; }, [entity]); // Determine which columns to display const columns = useMemo(() => { if (view?.visibleFieldKeys && view.visibleFieldKeys.length > 0) { return view.visibleFieldKeys .map((k) => fieldKeyMap.get(k)) .filter((f): f is FieldDefinition => Boolean(f)); } // Fallback: first 6 non-hidden fields return entity.fields.filter((f) => !f.hidden).slice(0, 6); }, [view, fieldKeyMap, entity]); // Handle sort click const handleHeaderClick = (key: string) => { if (sortField === key) { setSortOrder((prev) => (prev === 'asc' ? 'desc' : 'asc')); } else { setSortField(key); setSortOrder('asc'); } }; // Filter & sort const filteredRecords = useMemo(() => { let result = [...records]; // Search filter if (searchQuery.trim()) { const q = searchQuery.toLowerCase().trim(); result = result.filter((row) => { return Object.values(row).some((val) => { if (typeof val === 'string') return val.toLowerCase().includes(q); if (typeof val === 'number') return String(val).includes(q); return false; }); }); } // Sort if (sortField) { result.sort((a, b) => { const aVal = a[sortField]; const bVal = b[sortField]; if (aVal === bVal) return 0; if (aVal === undefined || aVal === null) return 1; if (bVal === undefined || bVal === null) return -1; const comp = aVal > bVal ? 1 : -1; return sortOrder === 'desc' ? -comp : comp; }); } return result; }, [records, searchQuery, sortField, sortOrder]); // Paginated records const totalPages = Math.ceil(filteredRecords.length / pageSize) || 1; const paginatedRecords = useMemo(() => { const start = (currentPage - 1) * pageSize; return filteredRecords.slice(start, start + pageSize); }, [filteredRecords, currentPage, pageSize]); return (
{/* Table Top Controls */}
{ setSearchQuery(e.target.value); setCurrentPage(1); }} placeholder="Поиск по записям..." icon={} className="text-xs py-1.5" />
Найдено записей: {filteredRecords.length} {onCreateNew && ( )}
{/* Mobile Card List View (< 768px) */}
{paginatedRecords.map((row) => { const primaryCol = columns.find((c) => c.key === entity.primaryFieldKey) || columns[0]; const primaryVal = primaryCol ? formatFieldValue(primaryCol.type, row[primaryCol.key], primaryCol) : row.id; const statusCol = columns.find((c) => c.key === 'status' || c.type === 'status'); const statusVal = statusCol ? formatFieldValue(statusCol.type, row[statusCol.key], statusCol) : null; const otherCols = columns.filter((c) => c.key !== primaryCol?.key && c.key !== statusCol?.key).slice(0, 4); return (
onRowClick && onRowClick(row)} className="p-3.5 bg-krona-surface border border-krona-border rounded-xl shadow-xs space-y-2.5 active:bg-krona-workspace/60 transition-colors cursor-pointer" >
{primaryVal}
ID: {row.id.slice(0, 10)}...
{statusVal && ( {statusVal} )}
{otherCols.length > 0 && (
{otherCols.map((c) => (
{c.label}
{formatFieldValue(c.type, row[c.key], c)}
))}
)} {/* Mobile Card Actions */}
e.stopPropagation()} > {onRowClick && ( )} {onEdit && ( )} {onDelete && ( )}
); })} {paginatedRecords.length === 0 && !isLoading && (

Записей пока нет

{onCreateNew && ( )}
)}
{/* Desktop/Tablet Table Element (>= 768px) */}
{columns.map((col) => { const isSorted = sortField === col.key; return ( ); })} {paginatedRecords.map((row) => ( onRowClick && onRowClick(row)} className="hover:bg-krona-workspace/60 transition-colors cursor-pointer group" > {columns.map((col) => { const val = row[col.key]; const display = formatFieldValue(col.type, val, col); return ( ); })} {/* Actions Column */} ))} {paginatedRecords.length === 0 && !isLoading && ( )}
handleHeaderClick(col.key)} className="p-3 font-semibold text-krona-primary hover:bg-krona-surface-muted/60 cursor-pointer transition-colors whitespace-nowrap" >
{col.label} {isSorted ? ( sortOrder === 'asc' ? ( ) : ( ) ) : ( )}
Действия
{col.type === 'color' && val ? (
{display}
) : col.type === 'boolean' && val !== undefined && val !== null ? ( {val ? 'Да' : 'Нет'} ) : col.key === 'status' || col.type === 'status' ? ( {display} ) : col.key === entity.primaryFieldKey || col.key === 'name' ? ( {display} ) : ( {display} )}
e.stopPropagation()} >
{onRowClick && ( )} {onEdit && ( )} {onDelete && ( )}

Записей пока нет

Создайте первую запись в сущности «{entity.name}»

{onCreateNew && ( )}
{/* Pagination Footer */} {totalPages > 1 && (
Страница {currentPage} из {totalPages}
)}
); }; export default DynamicTable;