Files
myeasycms-v2/packages/features/module-builder/src/components/module-table.tsx
2026-03-29 19:44:57 +02:00

194 lines
6.2 KiB
TypeScript

'use client';
import type { CmsFieldType } from '../schema/module.schema';
interface FieldDefinition {
name: string;
display_name: string;
field_type: CmsFieldType;
show_in_table: boolean;
is_sortable: boolean;
sort_order: number;
}
interface ModuleRecord {
id: string;
data: Record<string, unknown>;
status: string;
created_at: string;
updated_at: string;
}
interface Pagination {
page: number;
pageSize: number;
total: number;
totalPages: number;
}
interface ModuleTableProps {
fields: FieldDefinition[];
records: ModuleRecord[];
pagination: Pagination;
onPageChange: (page: number) => void;
onSort: (field: string, direction: 'asc' | 'desc') => void;
onRowClick?: (recordId: string) => void;
selectedIds?: Set<string>;
onSelectionChange?: (ids: Set<string>) => void;
currentSort?: { field: string; direction: 'asc' | 'desc' };
}
/**
* Dynamic data table driven by module field definitions.
* Replaces the legacy my_modulklasse table rendering.
* Uses native table for now; Phase 3 enhancement will use @kit/ui/data-table.
*/
export function ModuleTable({
fields,
records,
pagination,
onPageChange,
onSort,
onRowClick,
selectedIds = new Set(),
onSelectionChange,
currentSort,
}: ModuleTableProps) {
const visibleFields = fields
.filter((f) => f.show_in_table)
.sort((a, b) => a.sort_order - b.sort_order);
const formatCellValue = (value: unknown, fieldType: CmsFieldType): string => {
if (value == null || value === '') return '—';
switch (fieldType) {
case 'checkbox':
return value ? '✓' : '✗';
case 'currency':
return new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(Number(value));
case 'date':
try { return new Date(String(value)).toLocaleDateString('de-DE'); } catch { return String(value); }
case 'password':
return '••••••';
default:
return String(value);
}
};
const handleSort = (fieldName: string) => {
const newDirection = currentSort?.field === fieldName && currentSort.direction === 'asc'
? 'desc' : 'asc';
onSort(fieldName, newDirection);
};
return (
<div className="space-y-4">
<div className="rounded-md border overflow-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b bg-muted/50">
{onSelectionChange && (
<th className="p-3 w-10">
<input
type="checkbox"
checked={records.length > 0 && records.every((r) => selectedIds.has(r.id))}
onChange={(e) => {
if (e.target.checked) {
onSelectionChange(new Set(records.map((r) => r.id)));
} else {
onSelectionChange(new Set());
}
}}
/>
</th>
)}
{visibleFields.map((field) => (
<th
key={field.name}
className="p-3 text-left font-medium cursor-pointer hover:bg-muted/80 select-none"
onClick={() => field.is_sortable && handleSort(field.name)}
>
<span className="flex items-center gap-1">
{field.display_name}
{currentSort?.field === field.name && (
<span className="text-xs">{currentSort.direction === 'asc' ? '↑' : '↓'}</span>
)}
</span>
</th>
))}
</tr>
</thead>
<tbody>
{records.length === 0 ? (
<tr>
<td
colSpan={visibleFields.length + (onSelectionChange ? 1 : 0)}
className="p-8 text-center text-muted-foreground"
>
Keine Datensätze gefunden
</td>
</tr>
) : (
records.map((record) => (
<tr
key={record.id}
className="border-b hover:bg-muted/30 cursor-pointer transition-colors"
onClick={() => onRowClick?.(record.id)}
>
{onSelectionChange && (
<td className="p-3" onClick={(e) => e.stopPropagation()}>
<input
type="checkbox"
checked={selectedIds.has(record.id)}
onChange={(e) => {
const next = new Set(selectedIds);
if (e.target.checked) {
next.add(record.id);
} else {
next.delete(record.id);
}
onSelectionChange(next);
}}
/>
</td>
)}
{visibleFields.map((field) => (
<td key={field.name} className="p-3">
{formatCellValue(record.data[field.name], field.field_type)}
</td>
))}
</tr>
))
)}
</tbody>
</table>
</div>
{/* Pagination */}
{pagination.totalPages > 1 && (
<div className="flex items-center justify-between px-2">
<span className="text-sm text-muted-foreground">
{pagination.total} Datensätze Seite {pagination.page} von {pagination.totalPages}
</span>
<div className="flex gap-1">
<button
onClick={() => onPageChange(pagination.page - 1)}
disabled={pagination.page <= 1}
className="rounded border px-3 py-1 text-sm hover:bg-muted disabled:opacity-50"
>
Zurück
</button>
<button
onClick={() => onPageChange(pagination.page + 1)}
disabled={pagination.page >= pagination.totalPages}
className="rounded border px-3 py-1 text-sm hover:bg-muted disabled:opacity-50"
>
Weiter
</button>
</div>
</div>
)}
</div>
);
}