Initial state for GitNexus analysis
This commit is contained in:
193
packages/features/module-builder/src/components/module-table.tsx
Normal file
193
packages/features/module-builder/src/components/module-table.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user