Remove duplicate compliance and DSGVO admin pages that have been superseded by the unified SDK pipeline. Update navigation, sidebar, roles, and module registry to reflect the new structure. Add DSFA corpus API proxy and source-policy components. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
318 lines
14 KiB
TypeScript
318 lines
14 KiB
TypeScript
'use client'
|
|
|
|
import Link from 'next/link'
|
|
import { usePathname } from 'next/navigation'
|
|
import { useState, useEffect } from 'react'
|
|
import { navigation, metaModules, NavCategory, CategoryId } from '@/lib/navigation'
|
|
import { RoleId, getStoredRole, isCategoryVisibleForRole } from '@/lib/roles'
|
|
import { RoleIndicator } from './RoleIndicator'
|
|
|
|
// Icons mapping
|
|
const categoryIcons: Record<string, React.ReactNode> = {
|
|
'shield-check': (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
|
</svg>
|
|
),
|
|
'clipboard-check': (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
|
|
</svg>
|
|
),
|
|
shield: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
|
</svg>
|
|
),
|
|
brain: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
|
</svg>
|
|
),
|
|
server: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
|
|
</svg>
|
|
),
|
|
graduation: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 14l9-5-9-5-9 5 9 5z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 14l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 14l9-5-9-5-9 5 9 5zm0 0l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998a12.078 12.078 0 01.665-6.479L12 14zm-4 6v-7.5l4-2.222" />
|
|
</svg>
|
|
),
|
|
mail: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
|
</svg>
|
|
),
|
|
code: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
|
|
</svg>
|
|
),
|
|
globe: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
|
|
</svg>
|
|
),
|
|
'code-2': (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
|
|
</svg>
|
|
),
|
|
}
|
|
|
|
const metaIcons: Record<string, React.ReactNode> = {
|
|
dashboard: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
|
</svg>
|
|
),
|
|
architecture: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
|
</svg>
|
|
),
|
|
onboarding: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
|
</svg>
|
|
),
|
|
backlog: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
|
|
</svg>
|
|
),
|
|
rbac: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
|
|
</svg>
|
|
),
|
|
}
|
|
|
|
interface SidebarProps {
|
|
onRoleChange?: () => void
|
|
}
|
|
|
|
export function Sidebar({ onRoleChange }: SidebarProps) {
|
|
const pathname = usePathname()
|
|
const [collapsed, setCollapsed] = useState(false)
|
|
const [expandedCategories, setExpandedCategories] = useState<Set<CategoryId>>(new Set())
|
|
const [currentRole, setCurrentRole] = useState<RoleId | null>(null)
|
|
|
|
useEffect(() => {
|
|
const role = getStoredRole()
|
|
setCurrentRole(role)
|
|
// Auto-expand category based on current path
|
|
if (role) {
|
|
const category = navigation.find(cat =>
|
|
cat.modules.some(m => pathname.startsWith(m.href.split('/')[1] ? `/${m.href.split('/')[1]}` : m.href))
|
|
)
|
|
if (category) {
|
|
setExpandedCategories(new Set([category.id]))
|
|
}
|
|
}
|
|
}, [pathname])
|
|
|
|
const toggleCategory = (categoryId: CategoryId) => {
|
|
setExpandedCategories(prev => {
|
|
const newSet = new Set(prev)
|
|
if (newSet.has(categoryId)) {
|
|
newSet.delete(categoryId)
|
|
} else {
|
|
newSet.add(categoryId)
|
|
}
|
|
return newSet
|
|
})
|
|
}
|
|
|
|
const isModuleActive = (href: string) => {
|
|
if (href === '/dashboard') {
|
|
return pathname === '/dashboard'
|
|
}
|
|
return pathname.startsWith(href)
|
|
}
|
|
|
|
const visibleCategories = currentRole
|
|
? navigation.filter(cat => isCategoryVisibleForRole(cat.id, currentRole))
|
|
: navigation
|
|
|
|
return (
|
|
<aside
|
|
className={`${
|
|
collapsed ? 'w-16' : 'w-64'
|
|
} bg-slate-900 text-white flex flex-col transition-all duration-300 fixed h-full z-20`}
|
|
>
|
|
{/* Logo/Header */}
|
|
<div className="h-16 flex items-center justify-between px-4 border-b border-slate-700">
|
|
{!collapsed && (
|
|
<Link href="/dashboard" className="font-bold text-lg">
|
|
Admin v2
|
|
</Link>
|
|
)}
|
|
<button
|
|
onClick={() => setCollapsed(!collapsed)}
|
|
className="p-2 rounded-lg hover:bg-slate-800 transition-colors"
|
|
title={collapsed ? 'Sidebar erweitern' : 'Sidebar einklappen'}
|
|
>
|
|
<svg
|
|
className={`w-5 h-5 transition-transform ${collapsed ? 'rotate-180' : ''}`}
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Navigation */}
|
|
<nav className="flex-1 py-4 overflow-y-auto">
|
|
{/* Meta Modules */}
|
|
<div className="px-2 mb-4">
|
|
{metaModules.map((module) => (
|
|
<Link
|
|
key={module.id}
|
|
href={module.href}
|
|
className={`flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors ${
|
|
isModuleActive(module.href)
|
|
? 'bg-primary-600 text-white'
|
|
: 'text-slate-300 hover:bg-slate-800 hover:text-white'
|
|
}`}
|
|
title={collapsed ? module.name : undefined}
|
|
>
|
|
<span className="flex-shrink-0">{metaIcons[module.id]}</span>
|
|
{!collapsed && <span className="truncate">{module.name}</span>}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
|
|
{/* Divider */}
|
|
<div className="mx-4 border-t border-slate-700 my-2" />
|
|
|
|
{/* Categories */}
|
|
<div className="px-2 space-y-1">
|
|
{visibleCategories.map((category) => {
|
|
const categoryHref = category.id === 'compliance-sdk' ? '/sdk' : `/${category.id}`
|
|
const isCategoryActive = category.id === 'compliance-sdk'
|
|
? category.modules.some(m => pathname.startsWith(m.href))
|
|
: pathname.startsWith(categoryHref)
|
|
|
|
return (
|
|
<div key={category.id}>
|
|
<div
|
|
className={`flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors ${
|
|
isCategoryActive
|
|
? 'bg-slate-800 text-white'
|
|
: 'text-slate-300 hover:bg-slate-800 hover:text-white'
|
|
}`}
|
|
>
|
|
<Link
|
|
href={categoryHref}
|
|
className="flex items-center gap-3 flex-1 min-w-0"
|
|
title={collapsed ? category.name : undefined}
|
|
>
|
|
<span
|
|
className="flex-shrink-0"
|
|
style={{ color: category.color }}
|
|
>
|
|
{categoryIcons[category.icon]}
|
|
</span>
|
|
{!collapsed && (
|
|
<span className="flex-1 text-left truncate">{category.name}</span>
|
|
)}
|
|
</Link>
|
|
{!collapsed && (
|
|
<button
|
|
onClick={(e) => {
|
|
e.preventDefault()
|
|
toggleCategory(category.id)
|
|
}}
|
|
className="p-1 rounded hover:bg-slate-700 transition-colors"
|
|
title={expandedCategories.has(category.id) ? 'Einklappen' : 'Aufklappen'}
|
|
>
|
|
<svg
|
|
className={`w-4 h-4 transition-transform ${
|
|
expandedCategories.has(category.id) ? 'rotate-180' : ''
|
|
}`}
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Category Modules */}
|
|
{!collapsed && expandedCategories.has(category.id) && (
|
|
<div className="ml-4 mt-1 space-y-1">
|
|
{(() => {
|
|
// Group modules by subgroup
|
|
const subgroups = new Map<string | undefined, typeof category.modules>()
|
|
category.modules.forEach((module) => {
|
|
const key = module.subgroup
|
|
if (!subgroups.has(key)) {
|
|
subgroups.set(key, [])
|
|
}
|
|
subgroups.get(key)!.push(module)
|
|
})
|
|
|
|
return Array.from(subgroups.entries()).map(([subgroupName, modules]) => (
|
|
<div key={subgroupName || 'default'}>
|
|
{subgroupName && (
|
|
<div className="px-3 py-1.5 text-xs font-medium text-slate-500 uppercase tracking-wider mt-2 first:mt-0">
|
|
{subgroupName}
|
|
</div>
|
|
)}
|
|
{modules.map((module) => (
|
|
<Link
|
|
key={module.id}
|
|
href={module.href}
|
|
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors ${
|
|
isModuleActive(module.href)
|
|
? 'bg-primary-600 text-white'
|
|
: 'text-slate-400 hover:bg-slate-800 hover:text-white'
|
|
}`}
|
|
>
|
|
<span
|
|
className="w-1.5 h-1.5 rounded-full"
|
|
style={{ backgroundColor: category.color }}
|
|
/>
|
|
<span className="truncate">{module.name}</span>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
))
|
|
})()}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</nav>
|
|
|
|
{/* Footer with Role Indicator */}
|
|
<div className="p-4 border-t border-slate-700">
|
|
<RoleIndicator collapsed={collapsed} onRoleChange={onRoleChange} />
|
|
|
|
<a
|
|
href="https://macmini:3000/admin"
|
|
className={`flex items-center gap-3 px-3 py-2 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 transition-colors mt-2 ${
|
|
collapsed ? 'justify-center' : ''
|
|
}`}
|
|
title="Altes Admin (Port 3000)"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
|
</svg>
|
|
{!collapsed && <span>Altes Admin</span>}
|
|
</a>
|
|
</div>
|
|
</aside>
|
|
)
|
|
}
|