fix(admin-v2): Restore HEAD SDK files for compatibility with new pages
Restore the SDK context, types, and component files to the HEAD version since newer pages (company-profile, import) depend on these API changes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -102,11 +102,11 @@ export function CommandBar({ onClose }: CommandBarProps) {
|
||||
{
|
||||
id: 'action-new-usecase',
|
||||
type: 'ACTION',
|
||||
label: 'Neuen Use Case erstellen',
|
||||
description: 'Startet den Use Case Workshop Wizard',
|
||||
label: 'Neuen Anwendungsfall erstellen',
|
||||
description: 'Startet die Anwendungsfall-Erfassung',
|
||||
icon: icons.action,
|
||||
action: () => {
|
||||
goToStep('use-case-workshop')
|
||||
goToStep('use-case-assessment')
|
||||
onClose()
|
||||
},
|
||||
},
|
||||
|
||||
@@ -165,6 +165,12 @@ function QRCodeModal({ isOpen, onClose, sessionId }: QRModalProps) {
|
||||
}
|
||||
})
|
||||
|
||||
// Force HTTP for mobile access (SSL cert is for hostname, not IP)
|
||||
// This is safe because it's only used on the local network
|
||||
if (baseUrl.startsWith('https://')) {
|
||||
baseUrl = baseUrl.replace('https://', 'http://')
|
||||
}
|
||||
|
||||
const uploadPath = `/upload/sdk/${sessionId}`
|
||||
const fullUrl = `${baseUrl}${uploadPath}`
|
||||
setUploadUrl(fullUrl)
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
* SDK Pipeline Sidebar
|
||||
*
|
||||
* Floating Action Button mit Drawer zur Visualisierung der SDK-Pipeline.
|
||||
* Zeigt die zwei Phasen (Compliance Assessment & Dokumentengenerierung)
|
||||
* mit Fortschritt und ermöglicht schnelle Navigation.
|
||||
* Zeigt die 5 Pakete mit Fortschritt und ermoeglicht schnelle Navigation.
|
||||
*
|
||||
* Features:
|
||||
* - Desktop (xl+): Fixierte Sidebar rechts
|
||||
@@ -15,7 +14,7 @@
|
||||
import Link from 'next/link'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useSDK, SDK_STEPS, getStepsForPhase, type SDKStep } from '@/lib/sdk'
|
||||
import { useSDK, SDK_STEPS, SDK_PACKAGES, getStepsForPackage, type SDKStep, type SDKPackageId } from '@/lib/sdk'
|
||||
|
||||
// =============================================================================
|
||||
// ICONS
|
||||
@@ -27,6 +26,12 @@ const CheckIcon = () => (
|
||||
</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" />
|
||||
@@ -45,31 +50,6 @@ const PipelineIcon = () => (
|
||||
</svg>
|
||||
)
|
||||
|
||||
// Step Icons als Emojis
|
||||
const STEP_ICONS: Record<string, string> = {
|
||||
// Phase 1
|
||||
'use-case-workshop': '📋',
|
||||
'screening': '🔍',
|
||||
'modules': '📦',
|
||||
'requirements': '📜',
|
||||
'controls': '🛡️',
|
||||
'evidence': '📎',
|
||||
'audit-checklist': '✅',
|
||||
'risks': '⚠️',
|
||||
// Phase 2
|
||||
'ai-act': '🤖',
|
||||
'obligations': '📑',
|
||||
'dsfa': '📄',
|
||||
'tom': '🔒',
|
||||
'einwilligungen': '✍️',
|
||||
'loeschfristen': '🗑️',
|
||||
'vvt': '📊',
|
||||
'consent': '📝',
|
||||
'cookie-banner': '🍪',
|
||||
'dsr': '👤',
|
||||
'escalations': '🚨',
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// STEP ITEM
|
||||
// =============================================================================
|
||||
@@ -82,8 +62,6 @@ interface StepItemProps {
|
||||
}
|
||||
|
||||
function StepItem({ step, isActive, isCompleted, onNavigate }: StepItemProps) {
|
||||
const icon = STEP_ICONS[step.id] || '•'
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={step.url}
|
||||
@@ -96,7 +74,6 @@ function StepItem({ step, isActive, isCompleted, onNavigate }: StepItemProps) {
|
||||
: 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-gray-800'
|
||||
}`}
|
||||
>
|
||||
<span className="text-lg flex-shrink-0">{icon}</span>
|
||||
<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">
|
||||
@@ -111,78 +88,91 @@ function StepItem({ step, isActive, isCompleted, onNavigate }: StepItemProps) {
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PHASE SECTION
|
||||
// PACKAGE SECTION
|
||||
// =============================================================================
|
||||
|
||||
interface PhaseSectionProps {
|
||||
phase: 1 | 2
|
||||
title: string
|
||||
interface PackageSectionProps {
|
||||
pkg: (typeof SDK_PACKAGES)[number]
|
||||
steps: SDKStep[]
|
||||
completion: number
|
||||
currentStepId: string
|
||||
completedSteps: string[]
|
||||
isLocked: boolean
|
||||
onNavigate: () => void
|
||||
isExpanded: boolean
|
||||
onToggle: () => void
|
||||
}
|
||||
|
||||
function PhaseSection({
|
||||
phase,
|
||||
title,
|
||||
function PackageSection({
|
||||
pkg,
|
||||
steps,
|
||||
completion,
|
||||
currentStepId,
|
||||
completedSteps,
|
||||
isLocked,
|
||||
onNavigate,
|
||||
isExpanded,
|
||||
onToggle,
|
||||
}: PhaseSectionProps) {
|
||||
}: PackageSectionProps) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{/* Phase Header */}
|
||||
{/* Package Header */}
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="w-full flex items-center justify-between px-3 py-2 rounded-lg bg-slate-50 dark:bg-gray-800 hover:bg-slate-100 dark:hover:bg-gray-700 transition-colors"
|
||||
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 font-bold ${
|
||||
completion === 100
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
{completion === 100 ? <CheckIcon /> : phase}
|
||||
{isLocked ? <LockIcon /> : completion === 100 ? <CheckIcon /> : pkg.icon}
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="text-sm font-medium text-slate-700 dark:text-slate-200">{title}</div>
|
||||
<div className="text-xs text-slate-500 dark:text-slate-400">{completion}% abgeschlossen</div>
|
||||
<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>
|
||||
<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>
|
||||
{!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 */}
|
||||
<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}%` }}
|
||||
/>
|
||||
{!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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Steps List */}
|
||||
{isExpanded && (
|
||||
{isExpanded && !isLocked && (
|
||||
<div className="space-y-1 pl-2">
|
||||
{steps.map(step => (
|
||||
<StepItem
|
||||
@@ -210,39 +200,16 @@ function PipelineFlow() {
|
||||
Datenfluss
|
||||
</div>
|
||||
<div className="p-3 bg-slate-50 dark:bg-gray-900 rounded-lg">
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Phase 1 Flow */}
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-purple-600 dark:text-purple-400 font-medium">Phase 1</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<span title="Use Case">📋</span>
|
||||
<ArrowIcon />
|
||||
<span title="Screening">🔍</span>
|
||||
<ArrowIcon />
|
||||
<span title="Risiken">⚠️</span>
|
||||
<ArrowIcon />
|
||||
<span title="Controls">🛡️</span>
|
||||
<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>
|
||||
{/* Arrow Down */}
|
||||
<div className="flex justify-center">
|
||||
<svg className="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
|
||||
</svg>
|
||||
</div>
|
||||
{/* Phase 2 Flow */}
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-indigo-600 dark:text-indigo-400 font-medium">Phase 2</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<span title="DSFA">📄</span>
|
||||
<ArrowIcon />
|
||||
<span title="TOM">🔒</span>
|
||||
<ArrowIcon />
|
||||
<span title="VVT">📊</span>
|
||||
<ArrowIcon />
|
||||
<span title="Export">✅</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -259,60 +226,72 @@ interface SidebarContentProps {
|
||||
|
||||
function SidebarContent({ onNavigate }: SidebarContentProps) {
|
||||
const pathname = usePathname()
|
||||
const { state, phase1Completion, phase2Completion } = useSDK()
|
||||
const [expandedPhases, setExpandedPhases] = useState<Record<number, boolean>>({
|
||||
1: true,
|
||||
2: false,
|
||||
const { state, packageCompletion } = useSDK()
|
||||
const [expandedPackages, setExpandedPackages] = useState<Record<SDKPackageId, boolean>>({
|
||||
'vorbereitung': true,
|
||||
'analyse': false,
|
||||
'dokumentation': false,
|
||||
'rechtliche-texte': false,
|
||||
'betrieb': false,
|
||||
})
|
||||
|
||||
const phase1Steps = getStepsForPhase(1)
|
||||
const phase2Steps = getStepsForPhase(2)
|
||||
|
||||
// Find current step
|
||||
const currentStep = SDK_STEPS.find(s => s.url === pathname)
|
||||
const currentStepId = currentStep?.id || ''
|
||||
|
||||
// Auto-expand current phase
|
||||
// Auto-expand current package
|
||||
useEffect(() => {
|
||||
if (currentStep) {
|
||||
setExpandedPhases(prev => ({
|
||||
setExpandedPackages(prev => ({
|
||||
...prev,
|
||||
[currentStep.phase]: true,
|
||||
[currentStep.package]: true,
|
||||
}))
|
||||
}
|
||||
}, [currentStep])
|
||||
|
||||
const togglePhase = (phase: number) => {
|
||||
setExpandedPhases(prev => ({ ...prev, [phase]: !prev[phase] }))
|
||||
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">
|
||||
{/* Phase 1 */}
|
||||
<PhaseSection
|
||||
phase={1}
|
||||
title="Compliance Assessment"
|
||||
steps={phase1Steps}
|
||||
completion={phase1Completion}
|
||||
currentStepId={currentStepId}
|
||||
completedSteps={state.completedSteps}
|
||||
onNavigate={onNavigate}
|
||||
isExpanded={expandedPhases[1]}
|
||||
onToggle={() => togglePhase(1)}
|
||||
/>
|
||||
|
||||
{/* Phase 2 */}
|
||||
<PhaseSection
|
||||
phase={2}
|
||||
title="Dokumentengenerierung"
|
||||
steps={phase2Steps}
|
||||
completion={phase2Completion}
|
||||
currentStepId={currentStepId}
|
||||
completedSteps={state.completedSteps}
|
||||
onNavigate={onNavigate}
|
||||
isExpanded={expandedPhases[2]}
|
||||
onToggle={() => togglePhase(2)}
|
||||
/>
|
||||
{/* 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 />
|
||||
|
||||
@@ -3,7 +3,14 @@
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useSDK, SDK_STEPS, getStepsForPhase, SDKPhase } from '@/lib/sdk'
|
||||
import {
|
||||
useSDK,
|
||||
SDK_STEPS,
|
||||
SDK_PACKAGES,
|
||||
getStepsForPackage,
|
||||
type SDKPackageId,
|
||||
type SDKStep,
|
||||
} from '@/lib/sdk'
|
||||
|
||||
// =============================================================================
|
||||
// ICONS
|
||||
@@ -44,80 +51,6 @@ const CollapseIcon = ({ collapsed }: { collapsed: boolean }) => (
|
||||
</svg>
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// PHASE INDICATOR
|
||||
// =============================================================================
|
||||
|
||||
interface PhaseIndicatorProps {
|
||||
phase: SDKPhase
|
||||
title: string
|
||||
completion: number
|
||||
isActive: boolean
|
||||
isExpanded: boolean
|
||||
onToggle: () => void
|
||||
collapsed: boolean
|
||||
}
|
||||
|
||||
function PhaseIndicator({ phase, title, completion, isActive, isExpanded, onToggle, collapsed }: PhaseIndicatorProps) {
|
||||
if (collapsed) {
|
||||
return (
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className={`w-full flex items-center justify-center py-3 transition-colors ${
|
||||
isActive
|
||||
? 'bg-purple-50 border-l-4 border-purple-600'
|
||||
: 'hover:bg-gray-50 border-l-4 border-transparent'
|
||||
}`}
|
||||
title={`${title} (${completion}%)`}
|
||||
>
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold ${
|
||||
isActive
|
||||
? 'bg-purple-600 text-white'
|
||||
: completion === 100
|
||||
? 'bg-green-500 text-white'
|
||||
: 'bg-gray-200 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{completion === 100 ? <CheckIcon /> : phase}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className={`w-full flex items-center justify-between px-4 py-3 text-left transition-colors ${
|
||||
isActive
|
||||
? 'bg-purple-50 border-l-4 border-purple-600'
|
||||
: 'hover:bg-gray-50 border-l-4 border-transparent'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold ${
|
||||
isActive
|
||||
? 'bg-purple-600 text-white'
|
||||
: completion === 100
|
||||
? 'bg-green-500 text-white'
|
||||
: 'bg-gray-200 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{completion === 100 ? <CheckIcon /> : phase}
|
||||
</div>
|
||||
<div>
|
||||
<div className={`font-medium ${isActive ? 'text-purple-900' : 'text-gray-700'}`}>
|
||||
{title}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">{completion}% abgeschlossen</div>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronDownIcon className={`transition-transform ${isExpanded ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PROGRESS BAR
|
||||
// =============================================================================
|
||||
@@ -138,12 +71,108 @@ function ProgressBar({ value, className = '' }: ProgressBarProps) {
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PACKAGE INDICATOR
|
||||
// =============================================================================
|
||||
|
||||
interface PackageIndicatorProps {
|
||||
packageId: SDKPackageId
|
||||
order: number
|
||||
name: string
|
||||
icon: string
|
||||
completion: number
|
||||
isActive: boolean
|
||||
isExpanded: boolean
|
||||
isLocked: boolean
|
||||
onToggle: () => void
|
||||
collapsed: boolean
|
||||
}
|
||||
|
||||
function PackageIndicator({
|
||||
order,
|
||||
name,
|
||||
icon,
|
||||
completion,
|
||||
isActive,
|
||||
isExpanded,
|
||||
isLocked,
|
||||
onToggle,
|
||||
collapsed,
|
||||
}: PackageIndicatorProps) {
|
||||
if (collapsed) {
|
||||
return (
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className={`w-full flex items-center justify-center py-3 transition-colors ${
|
||||
isActive
|
||||
? 'bg-purple-50 border-l-4 border-purple-600'
|
||||
: isLocked
|
||||
? 'border-l-4 border-transparent opacity-50'
|
||||
: 'hover:bg-gray-50 border-l-4 border-transparent'
|
||||
}`}
|
||||
title={`${order}. ${name} (${completion}%)`}
|
||||
>
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center text-lg ${
|
||||
isLocked
|
||||
? 'bg-gray-200 text-gray-400'
|
||||
: isActive
|
||||
? 'bg-purple-600 text-white'
|
||||
: completion === 100
|
||||
? 'bg-green-500 text-white'
|
||||
: 'bg-gray-200 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{isLocked ? <LockIcon /> : completion === 100 ? <CheckIcon /> : icon}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onToggle}
|
||||
disabled={isLocked}
|
||||
className={`w-full flex items-center justify-between px-4 py-3 text-left transition-colors ${
|
||||
isLocked
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: isActive
|
||||
? 'bg-purple-50 border-l-4 border-purple-600'
|
||||
: 'hover:bg-gray-50 border-l-4 border-transparent'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center text-lg ${
|
||||
isLocked
|
||||
? 'bg-gray-200 text-gray-400'
|
||||
: isActive
|
||||
? 'bg-purple-600 text-white'
|
||||
: completion === 100
|
||||
? 'bg-green-500 text-white'
|
||||
: 'bg-gray-200 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{isLocked ? <LockIcon /> : completion === 100 ? <CheckIcon /> : icon}
|
||||
</div>
|
||||
<div>
|
||||
<div className={`font-medium text-sm ${isActive ? 'text-purple-900' : isLocked ? 'text-gray-400' : 'text-gray-700'}`}>
|
||||
{order}. {name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">{completion}%</div>
|
||||
</div>
|
||||
</div>
|
||||
{!isLocked && <ChevronDownIcon className={`transition-transform ${isExpanded ? 'rotate-180' : ''}`} />}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// STEP ITEM
|
||||
// =============================================================================
|
||||
|
||||
interface StepItemProps {
|
||||
step: (typeof SDK_STEPS)[number]
|
||||
step: SDKStep
|
||||
isActive: boolean
|
||||
isCompleted: boolean
|
||||
isLocked: boolean
|
||||
@@ -261,25 +290,48 @@ interface SDKSidebarProps {
|
||||
|
||||
export function SDKSidebar({ collapsed = false, onCollapsedChange }: SDKSidebarProps) {
|
||||
const pathname = usePathname()
|
||||
const { state, phase1Completion, phase2Completion, getCheckpointStatus } = useSDK()
|
||||
const [expandedPhases, setExpandedPhases] = React.useState<Record<number, boolean>>({
|
||||
1: true,
|
||||
2: true,
|
||||
const { state, packageCompletion, completionPercentage, getCheckpointStatus } = useSDK()
|
||||
const [expandedPackages, setExpandedPackages] = React.useState<Record<SDKPackageId, boolean>>({
|
||||
'vorbereitung': true,
|
||||
'analyse': false,
|
||||
'dokumentation': false,
|
||||
'rechtliche-texte': false,
|
||||
'betrieb': false,
|
||||
})
|
||||
|
||||
const togglePhase = (phase: number) => {
|
||||
setExpandedPhases(prev => ({ ...prev, [phase]: !prev[phase] }))
|
||||
// Auto-expand current package
|
||||
React.useEffect(() => {
|
||||
const currentStep = SDK_STEPS.find(s => s.url === pathname)
|
||||
if (currentStep) {
|
||||
setExpandedPackages(prev => ({
|
||||
...prev,
|
||||
[currentStep.package]: true,
|
||||
}))
|
||||
}
|
||||
}, [pathname])
|
||||
|
||||
const togglePackage = (packageId: SDKPackageId) => {
|
||||
setExpandedPackages(prev => ({ ...prev, [packageId]: !prev[packageId] }))
|
||||
}
|
||||
|
||||
const phase1Steps = getStepsForPhase(1)
|
||||
const phase2Steps = getStepsForPhase(2)
|
||||
|
||||
const isStepLocked = (step: (typeof SDK_STEPS)[number]): boolean => {
|
||||
const isStepLocked = (step: SDKStep): boolean => {
|
||||
if (state.preferences?.allowParallelWork) return false
|
||||
return step.prerequisiteSteps.some(prereq => !state.completedSteps.includes(prereq))
|
||||
}
|
||||
|
||||
const getStepCheckpointStatus = (step: (typeof SDK_STEPS)[number]): 'passed' | 'failed' | 'warning' | 'pending' => {
|
||||
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
|
||||
|
||||
// Check if previous package is complete
|
||||
const prevPkg = SDK_PACKAGES.find(p => p.order === pkg.order - 1)
|
||||
if (!prevPkg) return false
|
||||
|
||||
return packageCompletion[prevPkg.id] < 100
|
||||
}
|
||||
|
||||
const getStepCheckpointStatus = (step: SDKStep): 'passed' | 'failed' | 'warning' | 'pending' => {
|
||||
const status = getCheckpointStatus(step.checkpointId)
|
||||
if (!status) return 'pending'
|
||||
if (status.passed) return 'passed'
|
||||
@@ -288,6 +340,25 @@ export function SDKSidebar({ collapsed = false, onCollapsedChange }: SDKSidebarP
|
||||
return 'pending'
|
||||
}
|
||||
|
||||
const isStepActive = (stepUrl: string) => pathname === stepUrl
|
||||
|
||||
const isPackageActive = (packageId: SDKPackageId) => {
|
||||
const steps = getStepsForPackage(packageId)
|
||||
return steps.some(s => s.url === pathname)
|
||||
}
|
||||
|
||||
// Filter steps based on customer type
|
||||
const getVisibleSteps = (packageId: SDKPackageId): SDKStep[] => {
|
||||
const steps = getStepsForPackage(packageId)
|
||||
return steps.filter(step => {
|
||||
// Hide import step for new customers
|
||||
if (step.id === 'import' && state.customerType === 'new') {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className={`fixed left-0 top-0 h-screen ${collapsed ? 'w-16' : 'w-64'} bg-white border-r border-gray-200 flex flex-col z-40 transition-all duration-300`}>
|
||||
{/* Header */}
|
||||
@@ -317,71 +388,51 @@ export function SDKSidebar({ collapsed = false, onCollapsedChange }: SDKSidebarP
|
||||
<div className="px-4 py-3 border-b border-gray-100">
|
||||
<div className="flex items-center justify-between text-sm mb-2">
|
||||
<span className="text-gray-600">Gesamtfortschritt</span>
|
||||
<span className="font-medium text-purple-600">
|
||||
{Math.round((phase1Completion + phase2Completion) / 2)}%
|
||||
</span>
|
||||
<span className="font-medium text-purple-600">{completionPercentage}%</span>
|
||||
</div>
|
||||
<ProgressBar value={(phase1Completion + phase2Completion) / 2} />
|
||||
<ProgressBar value={completionPercentage} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Navigation */}
|
||||
{/* Navigation - 5 Packages */}
|
||||
<nav className="flex-1 overflow-y-auto">
|
||||
{/* Phase 1 */}
|
||||
<div>
|
||||
<PhaseIndicator
|
||||
phase={1}
|
||||
title="Compliance Assessment"
|
||||
completion={phase1Completion}
|
||||
isActive={state.currentPhase === 1}
|
||||
isExpanded={expandedPhases[1]}
|
||||
onToggle={() => togglePhase(1)}
|
||||
collapsed={collapsed}
|
||||
/>
|
||||
{expandedPhases[1] && (
|
||||
<div className="py-1">
|
||||
{phase1Steps.map(step => (
|
||||
<StepItem
|
||||
key={step.id}
|
||||
step={step}
|
||||
isActive={pathname === step.url}
|
||||
isCompleted={state.completedSteps.includes(step.id)}
|
||||
isLocked={isStepLocked(step)}
|
||||
checkpointStatus={getStepCheckpointStatus(step)}
|
||||
collapsed={collapsed}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{SDK_PACKAGES.map(pkg => {
|
||||
const steps = getVisibleSteps(pkg.id)
|
||||
const isLocked = isPackageLocked(pkg.id)
|
||||
const isActive = isPackageActive(pkg.id)
|
||||
|
||||
{/* Phase 2 */}
|
||||
<div className="border-t border-gray-100">
|
||||
<PhaseIndicator
|
||||
phase={2}
|
||||
title="Dokumentengenerierung"
|
||||
completion={phase2Completion}
|
||||
isActive={state.currentPhase === 2}
|
||||
isExpanded={expandedPhases[2]}
|
||||
onToggle={() => togglePhase(2)}
|
||||
collapsed={collapsed}
|
||||
/>
|
||||
{expandedPhases[2] && (
|
||||
<div className="py-1">
|
||||
{phase2Steps.map(step => (
|
||||
<StepItem
|
||||
key={step.id}
|
||||
step={step}
|
||||
isActive={pathname === step.url}
|
||||
isCompleted={state.completedSteps.includes(step.id)}
|
||||
isLocked={isStepLocked(step)}
|
||||
checkpointStatus={getStepCheckpointStatus(step)}
|
||||
collapsed={collapsed}
|
||||
/>
|
||||
))}
|
||||
return (
|
||||
<div key={pkg.id} className={pkg.order > 1 ? 'border-t border-gray-100' : ''}>
|
||||
<PackageIndicator
|
||||
packageId={pkg.id}
|
||||
order={pkg.order}
|
||||
name={pkg.name}
|
||||
icon={pkg.icon}
|
||||
completion={packageCompletion[pkg.id]}
|
||||
isActive={isActive}
|
||||
isExpanded={expandedPackages[pkg.id]}
|
||||
isLocked={isLocked}
|
||||
onToggle={() => togglePackage(pkg.id)}
|
||||
collapsed={collapsed}
|
||||
/>
|
||||
{expandedPackages[pkg.id] && !isLocked && (
|
||||
<div className="py-1">
|
||||
{steps.map(step => (
|
||||
<StepItem
|
||||
key={step.id}
|
||||
step={step}
|
||||
isActive={isStepActive(step.url)}
|
||||
isCompleted={state.completedSteps.includes(step.id)}
|
||||
isLocked={isStepLocked(step)}
|
||||
checkpointStatus={getStepCheckpointStatus(step)}
|
||||
collapsed={collapsed}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Additional Modules */}
|
||||
<div className="border-t border-gray-100 py-2">
|
||||
|
||||
Reference in New Issue
Block a user