Files
breakpilot-compliance/admin-compliance/app/sdk/sdk-flow/page.tsx
Benjamin Admin 215b95adfa
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 32s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 19s
refactor: Admin-Layout komplett entfernt — SDK als einziges Layout
Kaputtes (admin) Layout geloescht (Role-Selection, 404-Sidebar, localhost-Dashboard).
SDK-Flow nach /sdk/sdk-flow verschoben. Route-Gruppe (sdk) aufgeloest.
Root-Seite redirected auf /sdk. ~25 ungenutzte Dateien/Verzeichnisse entfernt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 11:43:00 +01:00

1020 lines
35 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
/**
* SDK Flow Visualization
*
* Interaktive React Flow Darstellung aller SDK-Steps:
* - Farblich nach Package gruppiert (5 Packages)
* - Prerequisite-Edges zeigen den sequenziellen Flow
* - DB-Tabellen und RAG-Collections als optionale Nodes
* - Detail-Panel bei Klick auf einen Step
*/
import { useCallback, useState, useMemo, useEffect } from 'react'
import ReactFlow, {
Node,
Edge,
Controls,
Background,
MiniMap,
useNodesState,
useEdgesState,
BackgroundVariant,
MarkerType,
Panel,
} from 'reactflow'
import 'reactflow/dist/style.css'
import {
SDK_FLOW_STEPS,
FLOW_PACKAGES,
findProducerStep,
getAllDbTables,
getAllRagCollections,
type SDKFlowStep,
} from './flow-data'
// =============================================================================
// TYPES
// =============================================================================
type PackageFilter = 'alle' | SDKFlowStep['package']
// =============================================================================
// LAYOUT
// =============================================================================
const PACKAGE_ORDER: SDKFlowStep['package'][] = [
'vorbereitung',
'analyse',
'dokumentation',
'rechtliche-texte',
'betrieb',
]
const PACKAGE_X_OFFSET: Record<string, number> = {
vorbereitung: 0,
analyse: 320,
dokumentation: 640,
'rechtliche-texte': 960,
betrieb: 1280,
}
const NODE_WIDTH = 200
const NODE_HEIGHT = 70
const STEP_Y_SPACING = 100
const STEP_Y_START = 100
function getStepPosition(step: SDKFlowStep): { x: number; y: number } {
const packageSteps = SDK_FLOW_STEPS
.filter(s => s.package === step.package)
.sort((a, b) => a.seq - b.seq)
const idx = packageSteps.findIndex(s => s.id === step.id)
return {
x: PACKAGE_X_OFFSET[step.package] + 40,
y: STEP_Y_START + idx * STEP_Y_SPACING,
}
}
// =============================================================================
// HELPERS — FERTIGSTELLUNGSGRAD
// =============================================================================
function completionColor(pct: number): string {
if (pct >= 70) return '#22c55e' // green
if (pct >= 50) return '#84cc16' // lime
if (pct >= 35) return '#f59e0b' // amber
return '#ef4444' // red
}
function completionLabel(pct: number): string {
if (pct >= 70) return 'Fortgeschritten'
if (pct >= 50) return 'In Entwicklung'
if (pct >= 35) return 'Begonnen'
return 'Frühe Phase'
}
// =============================================================================
// BETRIEB OVERVIEW PANEL
// =============================================================================
function BetriebOverviewPanel() {
const betriebWithCompletion = SDK_FLOW_STEPS.filter(
s => s.package === 'betrieb' && s.completion !== undefined
).sort((a, b) => (b.completion ?? 0) - (a.completion ?? 0))
const avg = Math.round(
betriebWithCompletion.reduce((sum, s) => sum + (s.completion ?? 0), 0) /
betriebWithCompletion.length
)
return (
<div className="bg-white rounded-xl border border-slate-200 shadow-sm p-5">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="font-semibold text-slate-800">
Betrieb Fertigstellungsgrad
</h3>
<p className="text-xs text-slate-400 mt-0.5">
{betriebWithCompletion.length} Module bewertet · Academy &amp; Training ausgeschlossen
</p>
</div>
<div className="text-right">
<div className="text-2xl font-bold" style={{ color: completionColor(avg) }}>
{avg}%
</div>
<div className="text-xs text-slate-400">Ø Fertigstellung</div>
</div>
</div>
<div className="space-y-3">
{betriebWithCompletion.map(step => {
const pct = step.completion ?? 0
const color = completionColor(pct)
return (
<div key={step.id}>
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-slate-700">{step.name}</span>
<span
className="px-1.5 py-0.5 rounded text-[10px] font-medium"
style={{ background: color + '22', color }}
>
{completionLabel(pct)}
</span>
</div>
<span className="text-sm font-bold" style={{ color }}>{pct}%</span>
</div>
<div className="h-2 bg-slate-100 rounded-full overflow-hidden">
<div
className="h-full rounded-full transition-all"
style={{ width: `${pct}%`, background: color }}
/>
</div>
<div className="flex items-center gap-3 mt-1 text-[10px] text-slate-400">
{step.checkpointId && (
<span className="bg-red-50 text-red-500 px-1 rounded">{step.checkpointId}</span>
)}
<span>{step.dbTables.length > 0 ? `DB: ${step.dbTables.length} Tabellen` : 'Kein DB-Backend'}</span>
{step.ragCollections.length > 0 && (
<span className="text-green-600">RAG: {step.ragCollections.length}</span>
)}
</div>
</div>
)
})}
</div>
</div>
)
}
// =============================================================================
// DETAIL PANEL
// =============================================================================
function DetailPanel({
step,
onClose,
}: {
step: SDKFlowStep
onClose: () => void
}) {
const pkg = FLOW_PACKAGES[step.package]
const baseUrl = 'https://macmini:3007'
return (
<div className="w-80 bg-white border-l border-slate-200 overflow-y-auto">
<div className="sticky top-0 bg-white z-10 border-b border-slate-200">
<div className="flex items-center justify-between p-4">
<h3 className="font-bold text-slate-900">{step.name}</h3>
<button
onClick={onClose}
className="text-slate-400 hover:text-slate-600 text-lg leading-none"
>
x
</button>
</div>
<div className="px-4 pb-3 flex items-center gap-2">
<span
className="px-2 py-0.5 rounded text-xs font-medium"
style={{ background: pkg.color.bg, color: pkg.color.text }}
>
{pkg.icon} {pkg.name}
</span>
<span className="text-xs text-slate-400">seq {step.seq}</span>
{step.isOptional && (
<span className="px-2 py-0.5 rounded text-xs bg-slate-100 text-slate-500">
Optional
</span>
)}
</div>
</div>
<div className="p-4 space-y-4">
{/* Fertigstellungsgrad */}
{step.completion !== undefined && (
<div className="bg-slate-50 rounded-lg p-3">
<div className="flex items-center justify-between mb-1.5">
<span className="text-xs font-semibold text-slate-500 uppercase">
Fertigstellungsgrad
</span>
<span
className="text-lg font-bold"
style={{ color: completionColor(step.completion) }}
>
{step.completion}%
</span>
</div>
<div className="h-2.5 bg-slate-200 rounded-full overflow-hidden">
<div
className="h-full rounded-full"
style={{
width: `${step.completion}%`,
background: completionColor(step.completion),
}}
/>
</div>
<div
className="text-xs mt-1.5 font-medium"
style={{ color: completionColor(step.completion) }}
>
{completionLabel(step.completion)}
</div>
</div>
)}
{/* Beschreibung */}
<div>
<p className="text-sm text-slate-700 leading-relaxed">{step.description}</p>
<p className="text-xs text-slate-500 leading-relaxed mt-2">{step.descriptionLong}</p>
{step.legalBasis && (
<div className="mt-2 px-2 py-1.5 bg-blue-50 rounded text-xs text-blue-700">
<span className="font-medium">Rechtsgrundlage:</span> {step.legalBasis}
</div>
)}
</div>
{/* Inputs */}
{step.inputs.length > 0 && (
<div>
<h4 className="text-xs font-semibold text-slate-500 uppercase mb-1.5">
Inputs (SDKState)
</h4>
<div className="space-y-1">
{step.inputs.map(input => {
const producer = findProducerStep(input)
return (
<div
key={input}
className="flex items-center justify-between text-sm bg-blue-50 rounded px-2 py-1"
>
<code className="text-blue-700 text-xs">{input}</code>
{producer && (
<span className="text-xs text-slate-400">
{producer.nameShort}
</span>
)}
</div>
)
})}
</div>
</div>
)}
{/* Outputs */}
{step.outputs.length > 0 && (
<div>
<h4 className="text-xs font-semibold text-slate-500 uppercase mb-1.5">
Outputs (SDKState)
</h4>
<div className="space-y-1">
{step.outputs.map(output => (
<div
key={output}
className="text-sm bg-emerald-50 rounded px-2 py-1"
>
<code className="text-emerald-700 text-xs">{output}</code>
</div>
))}
</div>
</div>
)}
{/* DB Tables */}
{step.dbTables.length > 0 && (
<div>
<h4 className="text-xs font-semibold text-slate-500 uppercase mb-1.5">
DB-Tabellen
</h4>
<div className="space-y-1">
{step.dbTables.map(table => (
<div
key={table}
className="flex items-center justify-between text-sm bg-slate-100 rounded px-2 py-1"
>
<code className="text-slate-700 text-xs">{table}</code>
<span className="text-xs text-slate-400">{step.dbMode}</span>
</div>
))}
</div>
</div>
)}
{/* RAG Collections */}
{step.ragCollections.length > 0 && (
<div>
<h4 className="text-xs font-semibold text-slate-500 uppercase mb-1.5">
RAG-Collections
</h4>
<div className="space-y-1">
{step.ragCollections.map(rag => (
<div
key={rag}
className="text-sm bg-green-50 rounded px-2 py-1"
>
<code className="text-green-700 text-xs">{rag}</code>
</div>
))}
{step.ragPurpose && (
<p className="text-xs text-slate-500 mt-1">{step.ragPurpose}</p>
)}
</div>
</div>
)}
{/* Checkpoint */}
{step.checkpointId && (
<div>
<h4 className="text-xs font-semibold text-slate-500 uppercase mb-1.5">
Checkpoint
</h4>
<div className="bg-amber-50 rounded p-2 space-y-1">
<div className="flex items-center gap-2">
<code className="text-xs font-bold text-amber-800">
{step.checkpointId}
</code>
<span
className={`px-1.5 py-0.5 rounded text-xs font-medium ${
step.checkpointType === 'REQUIRED'
? 'bg-red-100 text-red-700'
: 'bg-yellow-100 text-yellow-700'
}`}
>
{step.checkpointType}
</span>
</div>
{step.checkpointReviewer && step.checkpointReviewer !== 'NONE' && (
<div className="text-xs text-slate-600">
Reviewer: <span className="font-medium">{step.checkpointReviewer}</span>
</div>
)}
</div>
</div>
)}
{/* Generated Docs */}
{step.generates && step.generates.length > 0 && (
<div>
<h4 className="text-xs font-semibold text-slate-500 uppercase mb-1.5">
Generierte Dokumente
</h4>
<div className="space-y-1">
{step.generates.map(doc => (
<div
key={doc}
className="text-sm bg-violet-50 rounded px-2 py-1 text-violet-700"
>
{doc}
</div>
))}
</div>
</div>
)}
{/* Prerequisites */}
{step.prerequisiteSteps.length > 0 && (
<div>
<h4 className="text-xs font-semibold text-slate-500 uppercase mb-1.5">
Voraussetzungen
</h4>
<div className="space-y-1">
{step.prerequisiteSteps.map(preId => {
const preStep = SDK_FLOW_STEPS.find(s => s.id === preId)
return (
<div
key={preId}
className="text-sm text-slate-600 bg-slate-50 rounded px-2 py-1"
>
{preStep?.name || preId}
</div>
)
})}
</div>
</div>
)}
{/* Open in SDK */}
<button
onClick={() => window.open(`${baseUrl}${step.url}`, '_blank')}
className="w-full mt-2 px-4 py-2 bg-purple-600 text-white rounded-lg text-sm font-medium hover:bg-purple-700 transition-colors"
>
Im SDK oeffnen
</button>
</div>
</div>
)
}
// =============================================================================
// MAIN COMPONENT
// =============================================================================
export default function SDKFlowPage() {
const [selectedStep, setSelectedStep] = useState<SDKFlowStep | null>(null)
const [packageFilter, setPackageFilter] = useState<PackageFilter>('alle')
const [showDb, setShowDb] = useState(false)
const [showRag, setShowRag] = useState(false)
const allDbTables = useMemo(() => getAllDbTables(), [])
const allRagCollections = useMemo(() => getAllRagCollections(), [])
// =========================================================================
// Build Nodes
// =========================================================================
const { nodes: initialNodes, edges: initialEdges } = useMemo(() => {
const nodes: Node[] = []
const edges: Edge[] = []
const visibleSteps =
packageFilter === 'alle'
? SDK_FLOW_STEPS
: SDK_FLOW_STEPS.filter(s => s.package === packageFilter)
const visibleStepIds = new Set(visibleSteps.map(s => s.id))
// Step Nodes
visibleSteps.forEach(step => {
const pkg = FLOW_PACKAGES[step.package]
const pos = getStepPosition(step)
const isSelected = selectedStep?.id === step.id
// Checkpoint badge text
let badge = ''
if (step.checkpointId) {
badge = step.checkpointType === 'REQUIRED' ? ' *' : ' ~'
if (step.checkpointReviewer && step.checkpointReviewer !== 'NONE') {
badge += ` [${step.checkpointReviewer}]`
}
}
nodes.push({
id: step.id,
type: 'default',
position: pos,
data: {
label: (
<div className="text-center px-1">
<div className="font-medium text-xs leading-tight">
{step.nameShort}
</div>
<div className="text-[10px] opacity-70 mt-0.5">
{step.checkpointId || ''}
{badge}
</div>
{step.completion !== undefined && (
<div className="mt-1.5 px-1">
<div
className="flex items-center justify-center gap-1 text-[9px] font-bold mb-0.5"
style={{ color: isSelected ? 'rgba(255,255,255,0.9)' : completionColor(step.completion) }}
>
{step.completion}%
</div>
<div
className="h-1 rounded-full overflow-hidden"
style={{ background: isSelected ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.1)' }}
>
<div
className="h-full rounded-full"
style={{
width: `${step.completion}%`,
background: isSelected ? 'rgba(255,255,255,0.8)' : completionColor(step.completion),
}}
/>
</div>
</div>
)}
</div>
),
},
style: {
background: isSelected ? pkg.color.border : pkg.color.bg,
color: isSelected ? 'white' : pkg.color.text,
border: `2px solid ${pkg.color.border}`,
borderRadius: '10px',
padding: '8px 4px',
minWidth: `${NODE_WIDTH}px`,
maxWidth: `${NODE_WIDTH}px`,
cursor: 'pointer',
boxShadow: isSelected
? `0 0 16px ${pkg.color.border}`
: '0 1px 3px rgba(0,0,0,0.08)',
opacity: step.isOptional ? 0.85 : 1,
borderStyle: step.isOptional ? 'dashed' : 'solid',
},
})
})
// Prerequisite Edges
visibleSteps.forEach(step => {
step.prerequisiteSteps.forEach(preId => {
if (visibleStepIds.has(preId)) {
edges.push({
id: `e-${preId}-${step.id}`,
source: preId,
target: step.id,
type: 'smoothstep',
animated: selectedStep?.id === preId || selectedStep?.id === step.id,
style: {
stroke:
selectedStep?.id === preId || selectedStep?.id === step.id
? '#7c3aed'
: '#94a3b8',
strokeWidth:
selectedStep?.id === preId || selectedStep?.id === step.id
? 2.5
: 1.5,
},
markerEnd: {
type: MarkerType.ArrowClosed,
color:
selectedStep?.id === preId || selectedStep?.id === step.id
? '#7c3aed'
: '#94a3b8',
width: 14,
height: 14,
},
})
}
})
})
// DB Table Nodes
if (showDb) {
const dbTablesInUse = new Set<string>()
visibleSteps.forEach(s => s.dbTables.forEach(t => dbTablesInUse.add(t)))
let dbIdx = 0
dbTablesInUse.forEach(table => {
const nodeId = `db-${table}`
nodes.push({
id: nodeId,
type: 'default',
position: { x: -280, y: STEP_Y_START + dbIdx * 90 },
data: {
label: (
<div className="text-center">
<div className="text-sm mb-0.5">DB</div>
<div className="font-medium text-[10px] leading-tight">{table}</div>
</div>
),
},
style: {
background: '#f1f5f9',
color: '#475569',
border: '2px solid #94a3b8',
borderRadius: '50%',
width: '90px',
height: '90px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '4px',
},
})
// Connect steps to this DB table
visibleSteps
.filter(s => s.dbTables.includes(table))
.forEach(step => {
edges.push({
id: `e-db-${table}-${step.id}`,
source: nodeId,
target: step.id,
type: 'straight',
style: {
stroke: '#94a3b8',
strokeWidth: 1,
strokeDasharray: '6 3',
},
labelStyle: { fontSize: 9, fill: '#94a3b8' },
label: step.dbMode !== 'none' ? step.dbMode : undefined,
})
})
dbIdx++
})
}
// RAG Collection Nodes
if (showRag) {
const ragInUse = new Set<string>()
visibleSteps.forEach(s => s.ragCollections.forEach(r => ragInUse.add(r)))
let ragIdx = 0
ragInUse.forEach(collection => {
const nodeId = `rag-${collection}`
// Place RAG nodes to the right of all packages
const rightX = (packageFilter === 'alle' ? 1280 : PACKAGE_X_OFFSET[packageFilter] || 0) + NODE_WIDTH + 180
nodes.push({
id: nodeId,
type: 'default',
position: { x: rightX, y: STEP_Y_START + ragIdx * 90 },
data: {
label: (
<div className="text-center">
<div className="text-sm mb-0.5">RAG</div>
<div className="font-medium text-[10px] leading-tight">
{collection.replace('bp_', '')}
</div>
</div>
),
},
style: {
background: '#dcfce7',
color: '#166534',
border: '2px solid #22c55e',
borderRadius: '50%',
width: '90px',
height: '90px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '4px',
},
})
// Connect steps to this RAG collection
visibleSteps
.filter(s => s.ragCollections.includes(collection))
.forEach(step => {
edges.push({
id: `e-rag-${collection}-${step.id}`,
source: nodeId,
target: step.id,
type: 'straight',
style: {
stroke: '#22c55e',
strokeWidth: 1,
strokeDasharray: '6 3',
},
})
})
ragIdx++
})
}
return { nodes, edges }
}, [packageFilter, showDb, showRag, selectedStep])
// =========================================================================
// React Flow State
// =========================================================================
const [nodes, setNodes, onNodesChange] = useNodesState([])
const [edges, setEdges, onEdgesChange] = useEdgesState([])
useEffect(() => {
setNodes(initialNodes)
setEdges(initialEdges)
}, [initialNodes, initialEdges, setNodes, setEdges])
const onNodeClick = useCallback((_event: React.MouseEvent, node: Node) => {
const step = SDK_FLOW_STEPS.find(s => s.id === node.id)
if (step) {
setSelectedStep(prev => (prev?.id === step.id ? null : step))
}
}, [])
const onPaneClick = useCallback(() => {
setSelectedStep(null)
}, [])
// =========================================================================
// Stats
// =========================================================================
const stats = useMemo(() => {
const visible =
packageFilter === 'alle'
? SDK_FLOW_STEPS
: SDK_FLOW_STEPS.filter(s => s.package === packageFilter)
return {
total: SDK_FLOW_STEPS.length,
visible: visible.length,
checkpoints: visible.filter(s => s.checkpointId).length,
dbTables: allDbTables.length,
ragCollections: allRagCollections.length,
}
}, [packageFilter, allDbTables, allRagCollections])
// =========================================================================
// Render
// =========================================================================
return (
<div className="space-y-4">
{/* Header */}
<div className="bg-white rounded-xl border border-slate-200 p-5 shadow-sm">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-purple-600 flex items-center justify-center text-xl text-white">
SDK
</div>
<div>
<h2 className="text-xl font-bold text-slate-900">
SDK Flow Visualization
</h2>
<p className="text-sm text-slate-500">
{stats.total} Steps in 5 Packages | {stats.checkpoints} Checkpoints |{' '}
{stats.dbTables} DB-Tabellen | {stats.ragCollections} RAG-Collections
</p>
</div>
</div>
</div>
{/* Toolbar */}
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
<div className="flex flex-wrap items-center gap-2">
{/* Package Filter */}
<button
onClick={() => {
setPackageFilter('alle')
setSelectedStep(null)
}}
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
packageFilter === 'alle'
? 'bg-slate-800 text-white'
: 'bg-slate-100 text-slate-700 hover:bg-slate-200'
}`}
>
Alle ({SDK_FLOW_STEPS.length})
</button>
{PACKAGE_ORDER.map(pkgId => {
const pkg = FLOW_PACKAGES[pkgId]
const count = SDK_FLOW_STEPS.filter(s => s.package === pkgId).length
return (
<button
key={pkgId}
onClick={() => {
setPackageFilter(packageFilter === pkgId ? 'alle' : pkgId)
setSelectedStep(null)
}}
className="px-3 py-1.5 rounded-lg text-sm font-medium transition-all flex items-center gap-1.5"
style={{
background:
packageFilter === pkgId ? pkg.color.border : pkg.color.bg,
color: packageFilter === pkgId ? 'white' : pkg.color.text,
}}
>
<span
className="w-2.5 h-2.5 rounded-full"
style={{ background: pkg.color.border }}
/>
{pkg.nameShort} ({count})
</button>
)
})}
{/* Separator */}
<div className="w-px h-6 bg-slate-200 mx-1" />
{/* Toggles */}
<button
onClick={() => setShowDb(!showDb)}
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
showDb
? 'bg-slate-700 text-white'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
}`}
>
DB-Tabellen
</button>
<button
onClick={() => setShowRag(!showRag)}
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
showRag
? 'bg-green-600 text-white'
: 'bg-green-50 text-green-700 hover:bg-green-100'
}`}
>
RAG-Collections
</button>
</div>
</div>
{/* Flow Canvas + Detail Panel */}
<div className="flex bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden" style={{ height: '700px' }}>
{/* Canvas */}
<div className="flex-1 relative">
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onPaneClick={onPaneClick}
fitView
fitViewOptions={{ padding: 0.15 }}
attributionPosition="bottom-left"
>
<Controls />
<MiniMap
nodeColor={node => {
if (node.id.startsWith('db-')) return '#94a3b8'
if (node.id.startsWith('rag-')) return '#22c55e'
const step = SDK_FLOW_STEPS.find(s => s.id === node.id)
return step ? FLOW_PACKAGES[step.package].color.border : '#94a3b8'
}}
maskColor="rgba(0,0,0,0.08)"
/>
<Background variant={BackgroundVariant.Dots} gap={16} size={1} />
{/* Legende */}
<Panel
position="bottom-right"
className="bg-white/95 p-3 rounded-lg shadow-lg text-xs"
>
<div className="font-medium text-slate-700 mb-2">Legende</div>
<div className="space-y-1">
{PACKAGE_ORDER.map(pkgId => {
const pkg = FLOW_PACKAGES[pkgId]
return (
<div key={pkgId} className="flex items-center gap-2">
<span
className="w-3 h-3 rounded"
style={{
background: pkg.color.bg,
border: `1px solid ${pkg.color.border}`,
}}
/>
<span className="text-slate-600">{pkg.name}</span>
</div>
)
})}
<div className="border-t border-slate-200 my-1.5 pt-1.5">
<div className="flex items-center gap-2">
<span className="w-3 h-3 rounded-full bg-slate-200 border border-slate-400" />
<span className="text-slate-500">DB-Tabelle</span>
</div>
<div className="flex items-center gap-2 mt-0.5">
<span className="w-3 h-3 rounded-full bg-green-100 border border-green-500" />
<span className="text-slate-500">RAG-Collection</span>
</div>
</div>
<div className="border-t border-slate-200 my-1.5 pt-1.5 text-slate-400">
<div>* = REQUIRED</div>
<div>~ = RECOMMENDED</div>
<div>--- = gestrichelte Border: Optional</div>
</div>
</div>
</Panel>
{/* Package Headers */}
{packageFilter === 'alle' && (
<Panel position="top-center" className="flex gap-2 pointer-events-none">
{PACKAGE_ORDER.map((pkgId) => {
const pkg = FLOW_PACKAGES[pkgId]
return (
<div
key={pkgId}
className="px-3 py-1 rounded-lg text-xs font-medium opacity-60"
style={{
background: pkg.color.bg,
color: pkg.color.text,
border: `1px solid ${pkg.color.border}`,
minWidth: '200px',
textAlign: 'center',
}}
>
{pkg.icon} {pkg.name}
</div>
)
})}
</Panel>
)}
</ReactFlow>
</div>
{/* Detail Panel */}
{selectedStep && (
<DetailPanel
step={selectedStep}
onClose={() => setSelectedStep(null)}
/>
)}
</div>
{/* Betrieb Fertigstellungsgrad Übersicht */}
{packageFilter === 'betrieb' && <BetriebOverviewPanel />}
{/* Step Table (below flow) */}
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
<div className="px-4 py-3 bg-slate-50 border-b">
<h3 className="font-medium text-slate-700">
Alle Steps ({stats.visible}
{packageFilter !== 'alle' ? ` / ${stats.total}` : ''})
</h3>
</div>
<div className="divide-y max-h-96 overflow-y-auto">
{SDK_FLOW_STEPS.filter(
s => packageFilter === 'alle' || s.package === packageFilter
).map(step => {
const pkg = FLOW_PACKAGES[step.package]
return (
<button
key={step.id}
onClick={() => setSelectedStep(step)}
className={`w-full flex items-center gap-3 p-3 text-left transition-colors ${
selectedStep?.id === step.id
? 'bg-purple-50'
: 'hover:bg-slate-50'
}`}
>
<span
className="w-8 h-8 rounded-lg flex items-center justify-center text-xs font-bold shrink-0"
style={{ background: pkg.color.bg, color: pkg.color.text }}
>
{step.seq}
</span>
<div className="flex-1 min-w-0">
<div className="font-medium text-slate-800 text-sm">
{step.name}
</div>
<div className="text-xs text-slate-500 flex items-center gap-2 mt-0.5">
{step.checkpointId && (
<span
className={`px-1 py-0 rounded text-[10px] font-medium ${
step.checkpointType === 'REQUIRED'
? 'bg-red-100 text-red-600'
: 'bg-yellow-100 text-yellow-600'
}`}
>
{step.checkpointId}
</span>
)}
{step.dbTables.length > 0 && (
<span className="text-slate-400">
DB: {step.dbTables.join(', ')}
</span>
)}
{step.ragCollections.length > 0 && (
<span className="text-green-600">
RAG: {step.ragCollections.length}
</span>
)}
{step.isOptional && (
<span className="text-slate-400 italic">optional</span>
)}
</div>
</div>
{step.completion !== undefined ? (
<div className="shrink-0 w-24 mr-2">
<div className="flex items-center justify-between mb-0.5">
<span
className="text-[10px] font-bold"
style={{ color: completionColor(step.completion) }}
>
{step.completion}%
</span>
</div>
<div className="h-1.5 bg-slate-100 rounded-full overflow-hidden">
<div
className="h-full rounded-full"
style={{
width: `${step.completion}%`,
background: completionColor(step.completion),
}}
/>
</div>
</div>
) : (
<div className="shrink-0 w-24 mr-2" />
)}
<span
className="px-2 py-0.5 rounded text-[10px] font-medium shrink-0"
style={{ background: pkg.color.bg, color: pkg.color.text }}
>
{pkg.nameShort}
</span>
</button>
)
})}
</div>
</div>
</div>
)
}