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>
This commit is contained in:
121
admin-v2/components/catalog-manager/CatalogModuleTabs.tsx
Normal file
121
admin-v2/components/catalog-manager/CatalogModuleTabs.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
'use client'
|
||||
|
||||
import { useRef, useEffect } from 'react'
|
||||
import {
|
||||
ShieldAlert,
|
||||
FileText,
|
||||
Building2,
|
||||
Bot,
|
||||
BookOpen,
|
||||
Database,
|
||||
} from 'lucide-react'
|
||||
import type {
|
||||
CatalogModule,
|
||||
CatalogOverviewStats,
|
||||
} from '@/lib/sdk/catalog-manager/types'
|
||||
import { CATALOG_MODULE_LABELS } from '@/lib/sdk/catalog-manager/types'
|
||||
|
||||
interface CatalogModuleTabsProps {
|
||||
activeModule: CatalogModule | 'all'
|
||||
onModuleChange: (module: CatalogModule | 'all') => void
|
||||
stats: CatalogOverviewStats
|
||||
}
|
||||
|
||||
const MODULE_ICON_MAP: Record<CatalogModule, React.ComponentType<{ className?: string }>> = {
|
||||
dsfa: ShieldAlert,
|
||||
vvt: FileText,
|
||||
vendor: Building2,
|
||||
ai_act: Bot,
|
||||
reference: BookOpen,
|
||||
}
|
||||
|
||||
interface TabDefinition {
|
||||
key: CatalogModule | 'all'
|
||||
label: string
|
||||
icon: React.ComponentType<{ className?: string }>
|
||||
}
|
||||
|
||||
export default function CatalogModuleTabs({
|
||||
activeModule,
|
||||
onModuleChange,
|
||||
stats,
|
||||
}: CatalogModuleTabsProps) {
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const activeTabRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
// Scroll active tab into view on mount and when active changes
|
||||
useEffect(() => {
|
||||
if (activeTabRef.current && scrollRef.current) {
|
||||
const container = scrollRef.current
|
||||
const tab = activeTabRef.current
|
||||
const containerRect = container.getBoundingClientRect()
|
||||
const tabRect = tab.getBoundingClientRect()
|
||||
|
||||
if (tabRect.left < containerRect.left || tabRect.right > containerRect.right) {
|
||||
tab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' })
|
||||
}
|
||||
}
|
||||
}, [activeModule])
|
||||
|
||||
const tabs: TabDefinition[] = [
|
||||
{ key: 'all', label: 'Alle', icon: Database },
|
||||
...Object.entries(CATALOG_MODULE_LABELS).map(([key, label]) => ({
|
||||
key: key as CatalogModule,
|
||||
label,
|
||||
icon: MODULE_ICON_MAP[key as CatalogModule],
|
||||
})),
|
||||
]
|
||||
|
||||
const getCount = (key: CatalogModule | 'all'): number => {
|
||||
if (key === 'all') {
|
||||
return stats.totalEntries
|
||||
}
|
||||
return stats.byModule?.[key]?.entries ?? 0
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow">
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="flex overflow-x-auto scrollbar-hide border-b border-gray-200 dark:border-gray-700"
|
||||
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
|
||||
>
|
||||
{tabs.map(tab => {
|
||||
const isActive = activeModule === tab.key
|
||||
const Icon = tab.icon
|
||||
const count = getCount(tab.key)
|
||||
|
||||
return (
|
||||
<button
|
||||
key={tab.key}
|
||||
ref={isActive ? activeTabRef : undefined}
|
||||
onClick={() => onModuleChange(tab.key)}
|
||||
className={`relative flex items-center gap-2 px-4 py-3 text-sm font-medium whitespace-nowrap transition-colors shrink-0 ${
|
||||
isActive
|
||||
? 'text-violet-700 dark:text-violet-400 font-semibold'
|
||||
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'
|
||||
}`}
|
||||
>
|
||||
<Icon className="h-4 w-4 shrink-0" />
|
||||
<span>{tab.label}</span>
|
||||
<span
|
||||
className={`inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 text-xs font-medium rounded-full ${
|
||||
isActive
|
||||
? 'bg-violet-100 dark:bg-violet-900/40 text-violet-700 dark:text-violet-300'
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{count}
|
||||
</span>
|
||||
|
||||
{/* Active indicator line */}
|
||||
{isActive && (
|
||||
<span className="absolute bottom-0 left-0 right-0 h-0.5 bg-violet-600 dark:bg-violet-400 rounded-t" />
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user