194 lines
6.2 KiB
TypeScript
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>
|
|
);
|
|
}
|