This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/admin-v2/components/catalog-manager/CatalogTable.tsx
BreakPilot Dev 76b108a29f feat(admin-v2): Katalogverwaltung ins Admin-Dashboard integrieren
Katalogverwaltung von /sdk/catalog-manager nach /dashboard/catalog-manager
verschoben, damit sie im Admin-Dashboard mit Sidebar erscheint statt im
SDK-Bereich. Shared Components extrahiert, SDK-Route bleibt funktionsfaehig.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 12:12:38 +01:00

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>
)
}