Files
breakpilot-compliance/admin-compliance/components/sdk/SDKPipelineSidebar/SDKPipelineSidebarParts.tsx
Sharang Parnerkar b00fe6cb73 refactor(admin): split remaining components
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>
2026-04-17 12:41:29 +02:00

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