Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
255 lines
9.7 KiB
TypeScript
255 lines
9.7 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useMemo } from 'react'
|
|
import {
|
|
Search,
|
|
Plus,
|
|
Pencil,
|
|
Trash2,
|
|
Eye,
|
|
ChevronUp,
|
|
ChevronDown,
|
|
Database,
|
|
} from 'lucide-react'
|
|
import type { CatalogMeta, CatalogEntry } from '@/lib/sdk/catalog-manager/types'
|
|
|
|
interface CatalogTableProps {
|
|
catalog: CatalogMeta
|
|
entries: CatalogEntry[]
|
|
searchQuery: string
|
|
onSearchChange: (query: string) => void
|
|
onEditCustomEntry: (entry: CatalogEntry) => void
|
|
onDeleteCustomEntry: (entryId: string) => void
|
|
onAddEntry: () => void
|
|
}
|
|
|
|
type SortField = 'id' | 'name' | 'category' | 'type'
|
|
type SortDirection = 'asc' | 'desc'
|
|
|
|
export default function CatalogTable({
|
|
catalog,
|
|
entries,
|
|
searchQuery,
|
|
onSearchChange,
|
|
onEditCustomEntry,
|
|
onDeleteCustomEntry,
|
|
onAddEntry,
|
|
}: CatalogTableProps) {
|
|
const [sortField, setSortField] = useState<SortField>('name')
|
|
const [sortDirection, setSortDirection] = useState<SortDirection>('asc')
|
|
|
|
const handleSort = (field: SortField) => {
|
|
if (sortField === field) {
|
|
setSortDirection(prev => (prev === 'asc' ? 'desc' : 'asc'))
|
|
} else {
|
|
setSortField(field)
|
|
setSortDirection('asc')
|
|
}
|
|
}
|
|
|
|
const sortedEntries = useMemo(() => {
|
|
const sorted = [...entries].sort((a, b) => {
|
|
let aVal = ''
|
|
let bVal = ''
|
|
|
|
switch (sortField) {
|
|
case 'id':
|
|
aVal = a.id
|
|
bVal = b.id
|
|
break
|
|
case 'name':
|
|
aVal = a.displayName
|
|
bVal = b.displayName
|
|
break
|
|
case 'category':
|
|
aVal = a.category || ''
|
|
bVal = b.category || ''
|
|
break
|
|
case 'type':
|
|
aVal = a.source
|
|
bVal = b.source
|
|
break
|
|
}
|
|
|
|
const cmp = aVal.localeCompare(bVal, 'de')
|
|
return sortDirection === 'asc' ? cmp : -cmp
|
|
})
|
|
|
|
return sorted
|
|
}, [entries, sortField, sortDirection])
|
|
|
|
const SortIcon = ({ field }: { field: SortField }) => {
|
|
if (sortField !== field) {
|
|
return (
|
|
<span className="ml-1 inline-flex flex-col opacity-30">
|
|
<ChevronUp className="h-3 w-3 -mb-1" />
|
|
<ChevronDown className="h-3 w-3" />
|
|
</span>
|
|
)
|
|
}
|
|
return sortDirection === 'asc' ? (
|
|
<ChevronUp className="ml-1 h-3.5 w-3.5 inline" />
|
|
) : (
|
|
<ChevronDown className="ml-1 h-3.5 w-3.5 inline" />
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
{/* Search & Add Header */}
|
|
<div className="p-4 border-b border-gray-200 dark:border-gray-700 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
|
<div className="relative w-full sm:w-80">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
|
|
<input
|
|
type="text"
|
|
value={searchQuery}
|
|
onChange={e => onSearchChange(e.target.value)}
|
|
placeholder="Eintraege durchsuchen..."
|
|
className="w-full pl-9 pr-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-violet-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
{catalog.allowCustom && (
|
|
<button
|
|
onClick={onAddEntry}
|
|
className="inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium text-white bg-violet-600 hover:bg-violet-700 rounded-lg transition-colors shrink-0"
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
Eintrag hinzufuegen
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Table */}
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/50">
|
|
<th
|
|
className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400 cursor-pointer select-none hover:text-gray-700 dark:hover:text-gray-200 whitespace-nowrap"
|
|
onClick={() => handleSort('name')}
|
|
>
|
|
Name
|
|
<SortIcon field="name" />
|
|
</th>
|
|
{catalog.categoryField && (
|
|
<th
|
|
className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400 cursor-pointer select-none hover:text-gray-700 dark:hover:text-gray-200 whitespace-nowrap"
|
|
onClick={() => handleSort('category')}
|
|
>
|
|
Kategorie
|
|
<SortIcon field="category" />
|
|
</th>
|
|
)}
|
|
<th
|
|
className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400 cursor-pointer select-none hover:text-gray-700 dark:hover:text-gray-200 whitespace-nowrap"
|
|
onClick={() => handleSort('type')}
|
|
>
|
|
Quelle
|
|
<SortIcon field="type" />
|
|
</th>
|
|
<th className="px-4 py-3 text-right font-medium text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
|
Aktionen
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{sortedEntries.length === 0 ? (
|
|
<tr>
|
|
<td
|
|
colSpan={catalog.categoryField ? 4 : 3}
|
|
className="px-4 py-12 text-center text-gray-500 dark:text-gray-400"
|
|
>
|
|
<div className="flex flex-col items-center gap-2">
|
|
<Database className="h-8 w-8 opacity-40" />
|
|
<p className="text-sm">
|
|
{searchQuery
|
|
? `Keine Eintraege gefunden fuer "${searchQuery}"`
|
|
: 'Keine Eintraege vorhanden'}
|
|
</p>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
) : (
|
|
sortedEntries.map((entry) => (
|
|
<tr
|
|
key={`${entry.source}-${entry.id}`}
|
|
className="border-b border-gray-100 dark:border-gray-700/50 hover:bg-gray-50 dark:hover:bg-gray-700/30 transition-colors"
|
|
>
|
|
<td className="px-4 py-2.5">
|
|
<div>
|
|
<p className="text-gray-900 dark:text-gray-100 font-medium truncate max-w-xs">
|
|
{entry.displayName}
|
|
</p>
|
|
{entry.displayDescription && (
|
|
<p className="text-xs text-gray-500 dark:text-gray-400 truncate max-w-xs mt-0.5">
|
|
{entry.displayDescription}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</td>
|
|
{catalog.categoryField && (
|
|
<td className="px-4 py-2.5 text-gray-600 dark:text-gray-300">
|
|
{entry.category || '\u2014'}
|
|
</td>
|
|
)}
|
|
<td className="px-4 py-2.5">
|
|
{entry.source === 'system' ? (
|
|
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300">
|
|
System
|
|
</span>
|
|
) : (
|
|
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300">
|
|
Benutzerdefiniert
|
|
</span>
|
|
)}
|
|
</td>
|
|
<td className="px-4 py-2.5 text-right">
|
|
<div className="flex items-center justify-end gap-1">
|
|
{entry.source === 'system' ? (
|
|
<button
|
|
onClick={() => onEditCustomEntry(entry)}
|
|
className="inline-flex items-center gap-1 px-2 py-1 text-xs text-gray-600 dark:text-gray-400 hover:text-violet-600 dark:hover:text-violet-400 hover:bg-violet-50 dark:hover:bg-violet-900/20 rounded transition-colors"
|
|
title="Details anzeigen"
|
|
>
|
|
<Eye className="h-3.5 w-3.5" />
|
|
Details
|
|
</button>
|
|
) : (
|
|
<>
|
|
<button
|
|
onClick={() => onEditCustomEntry(entry)}
|
|
className="inline-flex items-center p-1.5 text-gray-500 dark:text-gray-400 hover:text-violet-600 dark:hover:text-violet-400 hover:bg-violet-50 dark:hover:bg-violet-900/20 rounded transition-colors"
|
|
title="Bearbeiten"
|
|
>
|
|
<Pencil className="h-3.5 w-3.5" />
|
|
</button>
|
|
<button
|
|
onClick={() => onDeleteCustomEntry(entry.id)}
|
|
className="inline-flex items-center p-1.5 text-gray-500 dark:text-gray-400 hover:text-red-600 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors"
|
|
title="Loeschen"
|
|
>
|
|
<Trash2 className="h-3.5 w-3.5" />
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{/* Footer with count */}
|
|
{sortedEntries.length > 0 && (
|
|
<div className="px-4 py-3 border-t border-gray-200 dark:border-gray-700 text-xs text-gray-500 dark:text-gray-400">
|
|
{sortedEntries.length} {sortedEntries.length === 1 ? 'Eintrag' : 'Eintraege'}
|
|
{searchQuery && ` (gefiltert)`}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|