Split 4 oversized component files (all >500 LOC) into sibling modules: - SDKPipelineSidebar → Icons + Parts siblings (193/264/35 LOC) - SourcesTab → SourceModals sibling (311/243 LOC) - ScopeDecisionTab → ScopeDecisionSections sibling (127/444 LOC) - ComplianceAdvisorWidget → ComplianceAdvisorParts sibling (265/131 LOC) Zero behavior changes; all logic relocated verbatim. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
265 lines
8.5 KiB
TypeScript
265 lines
8.5 KiB
TypeScript
'use client'
|
|
|
|
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'
|
|
import { CheckIcon, LockIcon, ArrowIcon } from './SDKPipelineSidebarIcons'
|
|
|
|
// =============================================================================
|
|
// STEP ITEM
|
|
// =============================================================================
|
|
|
|
interface StepItemProps {
|
|
step: SDKStep
|
|
isActive: boolean
|
|
isCompleted: boolean
|
|
onNavigate: () => void
|
|
}
|
|
|
|
export 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
|
|
}
|
|
|
|
export 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
|
|
// =============================================================================
|
|
|
|
export 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
|
|
}
|
|
|
|
export 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
|
|
}
|
|
|
|
// Filter steps based on visibleWhen conditions
|
|
const getVisibleStepsForPackage = (packageId: SDKPackageId): SDKStep[] => {
|
|
const steps = getStepsForPackage(packageId)
|
|
return steps.filter(step => {
|
|
if (step.visibleWhen) return step.visibleWhen(state)
|
|
return true
|
|
})
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Packages */}
|
|
{SDK_PACKAGES.map(pkg => (
|
|
<PackageSection
|
|
key={pkg.id}
|
|
pkg={pkg}
|
|
steps={getVisibleStepsForPackage(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>
|
|
)
|
|
}
|