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>
541 lines
18 KiB
TypeScript
541 lines
18 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* SDK Pipeline Sidebar
|
|
*
|
|
* Floating Action Button mit Drawer zur Visualisierung der SDK-Pipeline.
|
|
* Zeigt die 5 Pakete mit Fortschritt und ermoeglicht schnelle Navigation.
|
|
*
|
|
* Features:
|
|
* - Desktop (xl+): Fixierte Sidebar rechts
|
|
* - Mobile/Tablet: Floating Action Button mit Slide-In Drawer
|
|
*/
|
|
|
|
import Link from 'next/link'
|
|
import { useState, useEffect } from 'react'
|
|
import { usePathname } from 'next/navigation'
|
|
import { useSDK, SDK_STEPS, SDK_PACKAGES, getStepsForPackage, type SDKStep, type SDKPackageId } from '@/lib/sdk'
|
|
|
|
// =============================================================================
|
|
// ICONS
|
|
// =============================================================================
|
|
|
|
const CheckIcon = () => (
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
)
|
|
|
|
const LockIcon = () => (
|
|
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
</svg>
|
|
)
|
|
|
|
const ArrowIcon = () => (
|
|
<svg className="w-3 h-3 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
</svg>
|
|
)
|
|
|
|
const CloseIcon = () => (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
)
|
|
|
|
const PipelineIcon = () => (
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
|
|
</svg>
|
|
)
|
|
|
|
// =============================================================================
|
|
// STEP ITEM
|
|
// =============================================================================
|
|
|
|
interface StepItemProps {
|
|
step: SDKStep
|
|
isActive: boolean
|
|
isCompleted: boolean
|
|
onNavigate: () => void
|
|
}
|
|
|
|
function StepItem({ step, isActive, isCompleted, onNavigate }: StepItemProps) {
|
|
return (
|
|
<Link
|
|
href={step.url}
|
|
onClick={onNavigate}
|
|
className={`flex items-center gap-3 px-3 py-2 rounded-lg transition-all ${
|
|
isActive
|
|
? 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 font-medium'
|
|
: isCompleted
|
|
? 'text-green-600 dark:text-green-400 hover:bg-slate-100 dark:hover:bg-gray-800'
|
|
: 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-gray-800'
|
|
}`}
|
|
>
|
|
<span className="flex-1 text-sm truncate">{step.nameShort}</span>
|
|
{isCompleted && !isActive && (
|
|
<span className="flex-shrink-0 w-4 h-4 bg-green-500 text-white rounded-full flex items-center justify-center">
|
|
<CheckIcon />
|
|
</span>
|
|
)}
|
|
{isActive && (
|
|
<span className="flex-shrink-0 w-2 h-2 bg-purple-500 rounded-full animate-pulse" />
|
|
)}
|
|
</Link>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// PACKAGE SECTION
|
|
// =============================================================================
|
|
|
|
interface PackageSectionProps {
|
|
pkg: (typeof SDK_PACKAGES)[number]
|
|
steps: SDKStep[]
|
|
completion: number
|
|
currentStepId: string
|
|
completedSteps: string[]
|
|
isLocked: boolean
|
|
onNavigate: () => void
|
|
isExpanded: boolean
|
|
onToggle: () => void
|
|
}
|
|
|
|
function PackageSection({
|
|
pkg,
|
|
steps,
|
|
completion,
|
|
currentStepId,
|
|
completedSteps,
|
|
isLocked,
|
|
onNavigate,
|
|
isExpanded,
|
|
onToggle,
|
|
}: PackageSectionProps) {
|
|
return (
|
|
<div className="space-y-2">
|
|
{/* Package Header */}
|
|
<button
|
|
onClick={onToggle}
|
|
disabled={isLocked}
|
|
className={`w-full flex items-center justify-between px-3 py-2 rounded-lg transition-colors ${
|
|
isLocked
|
|
? 'bg-slate-100 dark:bg-gray-800 opacity-50 cursor-not-allowed'
|
|
: 'bg-slate-50 dark:bg-gray-800 hover:bg-slate-100 dark:hover:bg-gray-700'
|
|
}`}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<div
|
|
className={`w-7 h-7 rounded-full flex items-center justify-center text-sm ${
|
|
isLocked
|
|
? 'bg-gray-200 text-gray-400'
|
|
: completion === 100
|
|
? 'bg-green-500 text-white'
|
|
: 'bg-purple-600 text-white'
|
|
}`}
|
|
>
|
|
{isLocked ? <LockIcon /> : completion === 100 ? <CheckIcon /> : pkg.icon}
|
|
</div>
|
|
<div className="text-left">
|
|
<div className={`text-sm font-medium ${isLocked ? 'text-slate-400' : 'text-slate-700 dark:text-slate-200'}`}>
|
|
{pkg.order}. {pkg.nameShort}
|
|
</div>
|
|
<div className="text-xs text-slate-500 dark:text-slate-400">{completion}%</div>
|
|
</div>
|
|
</div>
|
|
{!isLocked && (
|
|
<svg
|
|
className={`w-4 h-4 text-slate-400 transition-transform ${isExpanded ? '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>
|
|
|
|
{/* Progress Bar */}
|
|
{!isLocked && (
|
|
<div className="px-3">
|
|
<div className="h-1.5 bg-slate-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
|
<div
|
|
className={`h-full rounded-full transition-all duration-500 ${
|
|
completion === 100 ? 'bg-green-500' : 'bg-purple-600'
|
|
}`}
|
|
style={{ width: `${completion}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Steps List */}
|
|
{isExpanded && !isLocked && (
|
|
<div className="space-y-1 pl-2">
|
|
{steps.map(step => (
|
|
<StepItem
|
|
key={step.id}
|
|
step={step}
|
|
isActive={currentStepId === step.id}
|
|
isCompleted={completedSteps.includes(step.id)}
|
|
onNavigate={onNavigate}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// PIPELINE FLOW VISUALIZATION
|
|
// =============================================================================
|
|
|
|
function PipelineFlow() {
|
|
return (
|
|
<div className="pt-3 border-t border-slate-200 dark:border-gray-700">
|
|
<div className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-2 px-1">
|
|
Datenfluss
|
|
</div>
|
|
<div className="p-3 bg-slate-50 dark:bg-gray-900 rounded-lg">
|
|
<div className="flex flex-col gap-1.5">
|
|
{SDK_PACKAGES.map((pkg, idx) => (
|
|
<div key={pkg.id} className="flex items-center gap-2 text-xs">
|
|
<span className="w-5 h-5 rounded-full bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center">
|
|
{pkg.icon}
|
|
</span>
|
|
<span className="text-slate-600 dark:text-slate-400 flex-1">{pkg.nameShort}</span>
|
|
{idx < SDK_PACKAGES.length - 1 && <ArrowIcon />}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// SIDEBAR CONTENT
|
|
// =============================================================================
|
|
|
|
interface SidebarContentProps {
|
|
onNavigate: () => void
|
|
}
|
|
|
|
function SidebarContent({ onNavigate }: SidebarContentProps) {
|
|
const pathname = usePathname()
|
|
const { state, packageCompletion } = useSDK()
|
|
const [expandedPackages, setExpandedPackages] = useState<Record<SDKPackageId, boolean>>({
|
|
'vorbereitung': true,
|
|
'analyse': false,
|
|
'dokumentation': false,
|
|
'rechtliche-texte': false,
|
|
'betrieb': false,
|
|
})
|
|
|
|
// Find current step
|
|
const currentStep = SDK_STEPS.find(s => s.url === pathname)
|
|
const currentStepId = currentStep?.id || ''
|
|
|
|
// Auto-expand current package
|
|
useEffect(() => {
|
|
if (currentStep) {
|
|
setExpandedPackages(prev => ({
|
|
...prev,
|
|
[currentStep.package]: true,
|
|
}))
|
|
}
|
|
}, [currentStep])
|
|
|
|
const togglePackage = (packageId: SDKPackageId) => {
|
|
setExpandedPackages(prev => ({ ...prev, [packageId]: !prev[packageId] }))
|
|
}
|
|
|
|
const isPackageLocked = (packageId: SDKPackageId): boolean => {
|
|
if (state.preferences?.allowParallelWork) return false
|
|
const pkg = SDK_PACKAGES.find(p => p.id === packageId)
|
|
if (!pkg || pkg.order === 1) return false
|
|
|
|
const prevPkg = SDK_PACKAGES.find(p => p.order === pkg.order - 1)
|
|
if (!prevPkg) return false
|
|
|
|
return packageCompletion[prevPkg.id] < 100
|
|
}
|
|
|
|
// Get visible steps based on customer type
|
|
const getVisibleSteps = (packageId: SDKPackageId): SDKStep[] => {
|
|
const steps = getStepsForPackage(packageId)
|
|
return steps.filter(step => {
|
|
if (step.id === 'import' && state.customerType === 'new') {
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Packages */}
|
|
{SDK_PACKAGES.map(pkg => (
|
|
<PackageSection
|
|
key={pkg.id}
|
|
pkg={pkg}
|
|
steps={getVisibleSteps(pkg.id)}
|
|
completion={packageCompletion[pkg.id]}
|
|
currentStepId={currentStepId}
|
|
completedSteps={state.completedSteps}
|
|
isLocked={isPackageLocked(pkg.id)}
|
|
onNavigate={onNavigate}
|
|
isExpanded={expandedPackages[pkg.id]}
|
|
onToggle={() => togglePackage(pkg.id)}
|
|
/>
|
|
))}
|
|
|
|
{/* Pipeline Flow */}
|
|
<PipelineFlow />
|
|
|
|
{/* Quick Info */}
|
|
{currentStep && (
|
|
<div className="pt-3 border-t border-slate-200 dark:border-gray-700">
|
|
<div className="text-xs text-slate-600 dark:text-slate-400 p-3 bg-slate-50 dark:bg-gray-800 rounded-lg">
|
|
<strong className="text-slate-700 dark:text-slate-300">Aktuell:</strong>{' '}
|
|
{currentStep.description}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// MAIN COMPONENT - RESPONSIVE
|
|
// =============================================================================
|
|
|
|
export interface SDKPipelineSidebarProps {
|
|
/** Position des FAB auf Mobile */
|
|
fabPosition?: 'bottom-right' | 'bottom-left'
|
|
}
|
|
|
|
export function SDKPipelineSidebar({ fabPosition = 'bottom-right' }: SDKPipelineSidebarProps) {
|
|
const [isMobileOpen, setIsMobileOpen] = useState(false)
|
|
const [isDesktopCollapsed, setIsDesktopCollapsed] = useState(true) // Start collapsed
|
|
const { completionPercentage } = useSDK()
|
|
|
|
// Load collapsed state from localStorage
|
|
useEffect(() => {
|
|
const stored = localStorage.getItem('sdk-pipeline-sidebar-collapsed')
|
|
if (stored !== null) {
|
|
setIsDesktopCollapsed(stored === 'true')
|
|
}
|
|
}, [])
|
|
|
|
// Save collapsed state to localStorage
|
|
const toggleDesktopSidebar = () => {
|
|
const newState = !isDesktopCollapsed
|
|
setIsDesktopCollapsed(newState)
|
|
localStorage.setItem('sdk-pipeline-sidebar-collapsed', String(newState))
|
|
}
|
|
|
|
// Close drawer on route change or escape key
|
|
useEffect(() => {
|
|
const handleEscape = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape') {
|
|
setIsMobileOpen(false)
|
|
setIsDesktopCollapsed(true)
|
|
}
|
|
}
|
|
window.addEventListener('keydown', handleEscape)
|
|
return () => window.removeEventListener('keydown', handleEscape)
|
|
}, [])
|
|
|
|
// Prevent body scroll when drawer is open
|
|
useEffect(() => {
|
|
if (isMobileOpen) {
|
|
document.body.style.overflow = 'hidden'
|
|
} else {
|
|
document.body.style.overflow = ''
|
|
}
|
|
return () => {
|
|
document.body.style.overflow = ''
|
|
}
|
|
}, [isMobileOpen])
|
|
|
|
const fabPositionClasses = fabPosition === 'bottom-right'
|
|
? 'right-4 bottom-6'
|
|
: 'left-4 bottom-6'
|
|
|
|
return (
|
|
<>
|
|
{/* Desktop: Fixed Sidebar (when expanded) */}
|
|
{!isDesktopCollapsed && (
|
|
<div className="hidden xl:block fixed right-6 top-24 w-72 z-10">
|
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-lg border border-slate-200 dark:border-gray-700 overflow-hidden">
|
|
{/* Header with close button */}
|
|
<div className="px-4 py-3 bg-gradient-to-r from-purple-50 to-indigo-50 dark:from-purple-900/20 dark:to-indigo-900/20 border-b border-slate-200 dark:border-gray-700">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-purple-600 dark:text-purple-400">
|
|
<PipelineIcon />
|
|
</span>
|
|
<div>
|
|
<span className="font-semibold text-slate-700 dark:text-slate-200 text-sm">
|
|
SDK Pipeline
|
|
</span>
|
|
<span className="ml-2 text-xs text-purple-600 dark:text-purple-400">
|
|
{completionPercentage}%
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={toggleDesktopSidebar}
|
|
className="p-1.5 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 rounded-lg hover:bg-slate-100 dark:hover:bg-gray-700 transition-colors"
|
|
aria-label="Sidebar einklappen"
|
|
title="Einklappen"
|
|
>
|
|
<CloseIcon />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="p-3 max-h-[calc(100vh-200px)] overflow-y-auto">
|
|
<SidebarContent onNavigate={() => {}} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Desktop: FAB (when collapsed) */}
|
|
{isDesktopCollapsed && (
|
|
<button
|
|
onClick={toggleDesktopSidebar}
|
|
className={`hidden xl:flex fixed right-6 bottom-6 z-40 w-14 h-14 bg-gradient-to-r from-purple-500 to-indigo-500 text-white rounded-full shadow-lg hover:shadow-xl transition-all items-center justify-center group`}
|
|
aria-label="SDK Pipeline Navigation oeffnen"
|
|
title="Pipeline anzeigen"
|
|
>
|
|
<PipelineIcon />
|
|
{/* Progress indicator */}
|
|
<svg
|
|
className="absolute inset-0 w-full h-full -rotate-90"
|
|
viewBox="0 0 56 56"
|
|
>
|
|
<circle
|
|
cx="28"
|
|
cy="28"
|
|
r="26"
|
|
fill="none"
|
|
stroke="rgba(255,255,255,0.3)"
|
|
strokeWidth="2"
|
|
/>
|
|
<circle
|
|
cx="28"
|
|
cy="28"
|
|
r="26"
|
|
fill="none"
|
|
stroke="white"
|
|
strokeWidth="2"
|
|
strokeDasharray={`${(completionPercentage / 100) * 163.36} 163.36`}
|
|
strokeLinecap="round"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
)}
|
|
|
|
{/* Mobile/Tablet: FAB */}
|
|
<button
|
|
onClick={() => setIsMobileOpen(true)}
|
|
className={`xl:hidden fixed ${fabPositionClasses} z-40 w-14 h-14 bg-gradient-to-r from-purple-500 to-indigo-500 text-white rounded-full shadow-lg hover:shadow-xl transition-all flex items-center justify-center group`}
|
|
aria-label="SDK Pipeline Navigation oeffnen"
|
|
>
|
|
<PipelineIcon />
|
|
{/* Progress indicator */}
|
|
<svg
|
|
className="absolute inset-0 w-full h-full -rotate-90"
|
|
viewBox="0 0 56 56"
|
|
>
|
|
<circle
|
|
cx="28"
|
|
cy="28"
|
|
r="26"
|
|
fill="none"
|
|
stroke="rgba(255,255,255,0.3)"
|
|
strokeWidth="2"
|
|
/>
|
|
<circle
|
|
cx="28"
|
|
cy="28"
|
|
r="26"
|
|
fill="none"
|
|
stroke="white"
|
|
strokeWidth="2"
|
|
strokeDasharray={`${(completionPercentage / 100) * 163.36} 163.36`}
|
|
strokeLinecap="round"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
|
|
{/* Mobile/Tablet: Drawer Overlay */}
|
|
{isMobileOpen && (
|
|
<div className="xl:hidden fixed inset-0 z-50">
|
|
{/* Backdrop */}
|
|
<div
|
|
className="absolute inset-0 bg-black/50 backdrop-blur-sm transition-opacity"
|
|
onClick={() => setIsMobileOpen(false)}
|
|
/>
|
|
|
|
{/* Drawer */}
|
|
<div className="absolute right-0 top-0 bottom-0 w-80 max-w-[85vw] bg-white dark:bg-gray-900 shadow-2xl transform transition-transform animate-slide-in-right">
|
|
{/* Drawer Header */}
|
|
<div className="flex items-center justify-between px-4 py-4 border-b border-slate-200 dark:border-gray-700 bg-gradient-to-r from-purple-50 to-indigo-50 dark:from-purple-900/20 dark:to-indigo-900/20">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-purple-600 dark:text-purple-400">
|
|
<PipelineIcon />
|
|
</span>
|
|
<div>
|
|
<span className="font-semibold text-slate-700 dark:text-slate-200">
|
|
SDK Pipeline
|
|
</span>
|
|
<span className="ml-2 text-sm text-purple-600 dark:text-purple-400">
|
|
{completionPercentage}%
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => setIsMobileOpen(false)}
|
|
className="p-2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 rounded-lg hover:bg-slate-100 dark:hover:bg-gray-800 transition-colors"
|
|
aria-label="Schliessen"
|
|
>
|
|
<CloseIcon />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Drawer Content */}
|
|
<div className="p-4 overflow-y-auto max-h-[calc(100vh-80px)]">
|
|
<SidebarContent onNavigate={() => setIsMobileOpen(false)} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* CSS for slide-in animation */}
|
|
<style jsx>{`
|
|
@keyframes slide-in-right {
|
|
from {
|
|
transform: translateX(100%);
|
|
}
|
|
to {
|
|
transform: translateX(0);
|
|
}
|
|
}
|
|
.animate-slide-in-right {
|
|
animation: slide-in-right 0.2s ease-out;
|
|
}
|
|
`}</style>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default SDKPipelineSidebar
|