refactor(admin): split sdk-flow page.tsx into colocated components
Extract BetriebOverviewPanel, DetailPanel, FlowCanvas, FlowToolbar, StepTable, useFlowGraph hook and helpers into _components/ so page.tsx drops from 1019 to 156 LOC (under the 300 soft target). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,74 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { SDK_FLOW_STEPS } from '../flow-data'
|
||||||
|
import { completionColor, completionLabel } from './helpers'
|
||||||
|
|
||||||
|
export 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 & 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
257
admin-compliance/app/sdk/sdk-flow/_components/DetailPanel.tsx
Normal file
257
admin-compliance/app/sdk/sdk-flow/_components/DetailPanel.tsx
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { SDK_FLOW_STEPS, FLOW_PACKAGES, findProducerStep, type SDKFlowStep } from '../flow-data'
|
||||||
|
import { completionColor, completionLabel } from './helpers'
|
||||||
|
|
||||||
|
export 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
142
admin-compliance/app/sdk/sdk-flow/_components/FlowCanvas.tsx
Normal file
142
admin-compliance/app/sdk/sdk-flow/_components/FlowCanvas.tsx
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import ReactFlow, {
|
||||||
|
Node,
|
||||||
|
Controls,
|
||||||
|
Background,
|
||||||
|
MiniMap,
|
||||||
|
BackgroundVariant,
|
||||||
|
Panel,
|
||||||
|
OnNodesChange,
|
||||||
|
OnEdgesChange,
|
||||||
|
Node as RFNode,
|
||||||
|
Edge as RFEdge,
|
||||||
|
} from 'reactflow'
|
||||||
|
import { SDK_FLOW_STEPS, FLOW_PACKAGES, type SDKFlowStep } from '../flow-data'
|
||||||
|
import { DetailPanel } from './DetailPanel'
|
||||||
|
import { PACKAGE_ORDER, type PackageFilter } from './helpers'
|
||||||
|
|
||||||
|
export function FlowCanvas({
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
onNodesChange,
|
||||||
|
onEdgesChange,
|
||||||
|
onNodeClick,
|
||||||
|
onPaneClick,
|
||||||
|
packageFilter,
|
||||||
|
selectedStep,
|
||||||
|
setSelectedStep,
|
||||||
|
}: {
|
||||||
|
nodes: RFNode[]
|
||||||
|
edges: RFEdge[]
|
||||||
|
onNodesChange: OnNodesChange
|
||||||
|
onEdgesChange: OnEdgesChange
|
||||||
|
onNodeClick: (e: React.MouseEvent, node: Node) => void
|
||||||
|
onPaneClick: () => void
|
||||||
|
packageFilter: PackageFilter
|
||||||
|
selectedStep: SDKFlowStep | null
|
||||||
|
setSelectedStep: (s: SDKFlowStep | null) => void
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { SDK_FLOW_STEPS, FLOW_PACKAGES } from '../flow-data'
|
||||||
|
import { PACKAGE_ORDER, type PackageFilter } from './helpers'
|
||||||
|
|
||||||
|
export function FlowToolbar({
|
||||||
|
packageFilter,
|
||||||
|
setPackageFilter,
|
||||||
|
setSelectedStep,
|
||||||
|
showDb,
|
||||||
|
setShowDb,
|
||||||
|
showRag,
|
||||||
|
setShowRag,
|
||||||
|
}: {
|
||||||
|
packageFilter: PackageFilter
|
||||||
|
setPackageFilter: (f: PackageFilter) => void
|
||||||
|
setSelectedStep: (s: null) => void
|
||||||
|
showDb: boolean
|
||||||
|
setShowDb: (v: boolean) => void
|
||||||
|
showRag: boolean
|
||||||
|
setShowRag: (v: boolean) => void
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
)
|
||||||
|
}
|
||||||
114
admin-compliance/app/sdk/sdk-flow/_components/StepTable.tsx
Normal file
114
admin-compliance/app/sdk/sdk-flow/_components/StepTable.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { SDK_FLOW_STEPS, FLOW_PACKAGES, type SDKFlowStep } from '../flow-data'
|
||||||
|
import { completionColor, type PackageFilter } from './helpers'
|
||||||
|
|
||||||
|
export function StepTable({
|
||||||
|
packageFilter,
|
||||||
|
selectedStep,
|
||||||
|
setSelectedStep,
|
||||||
|
visibleCount,
|
||||||
|
totalCount,
|
||||||
|
}: {
|
||||||
|
packageFilter: PackageFilter
|
||||||
|
selectedStep: SDKFlowStep | null
|
||||||
|
setSelectedStep: (s: SDKFlowStep) => void
|
||||||
|
visibleCount: number
|
||||||
|
totalCount: number
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<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 ({visibleCount}
|
||||||
|
{packageFilter !== 'alle' ? ` / ${totalCount}` : ''})
|
||||||
|
</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>
|
||||||
|
)
|
||||||
|
}
|
||||||
58
admin-compliance/app/sdk/sdk-flow/_components/helpers.ts
Normal file
58
admin-compliance/app/sdk/sdk-flow/_components/helpers.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { SDK_FLOW_STEPS, type SDKFlowStep } from '../flow-data'
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// LAYOUT
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const PACKAGE_ORDER: SDKFlowStep['package'][] = [
|
||||||
|
'vorbereitung',
|
||||||
|
'analyse',
|
||||||
|
'dokumentation',
|
||||||
|
'rechtliche-texte',
|
||||||
|
'betrieb',
|
||||||
|
]
|
||||||
|
|
||||||
|
export const PACKAGE_X_OFFSET: Record<string, number> = {
|
||||||
|
vorbereitung: 0,
|
||||||
|
analyse: 320,
|
||||||
|
dokumentation: 640,
|
||||||
|
'rechtliche-texte': 960,
|
||||||
|
betrieb: 1280,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NODE_WIDTH = 200
|
||||||
|
export const NODE_HEIGHT = 70
|
||||||
|
export const STEP_Y_SPACING = 100
|
||||||
|
export const STEP_Y_START = 100
|
||||||
|
|
||||||
|
export 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
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export 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
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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'
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PackageFilter = 'alle' | SDKFlowStep['package']
|
||||||
258
admin-compliance/app/sdk/sdk-flow/_components/useFlowGraph.tsx
Normal file
258
admin-compliance/app/sdk/sdk-flow/_components/useFlowGraph.tsx
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
import type { Node, Edge } from 'reactflow'
|
||||||
|
import { MarkerType } from 'reactflow'
|
||||||
|
import { SDK_FLOW_STEPS, FLOW_PACKAGES, type SDKFlowStep } from '../flow-data'
|
||||||
|
import {
|
||||||
|
NODE_WIDTH,
|
||||||
|
PACKAGE_X_OFFSET,
|
||||||
|
STEP_Y_START,
|
||||||
|
completionColor,
|
||||||
|
getStepPosition,
|
||||||
|
type PackageFilter,
|
||||||
|
} from './helpers'
|
||||||
|
|
||||||
|
export function useFlowGraph(
|
||||||
|
packageFilter: PackageFilter,
|
||||||
|
showDb: boolean,
|
||||||
|
showRag: boolean,
|
||||||
|
selectedStep: SDKFlowStep | null,
|
||||||
|
): { nodes: Node[]; edges: Edge[] } {
|
||||||
|
return 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])
|
||||||
|
}
|
||||||
@@ -11,424 +11,25 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback, useState, useMemo, useEffect } from 'react'
|
import { useCallback, useState, useMemo, useEffect } from 'react'
|
||||||
import ReactFlow, {
|
import {
|
||||||
Node,
|
Node,
|
||||||
Edge,
|
|
||||||
Controls,
|
|
||||||
Background,
|
|
||||||
MiniMap,
|
|
||||||
useNodesState,
|
useNodesState,
|
||||||
useEdgesState,
|
useEdgesState,
|
||||||
BackgroundVariant,
|
|
||||||
MarkerType,
|
|
||||||
Panel,
|
|
||||||
} from 'reactflow'
|
} from 'reactflow'
|
||||||
import 'reactflow/dist/style.css'
|
import 'reactflow/dist/style.css'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SDK_FLOW_STEPS,
|
SDK_FLOW_STEPS,
|
||||||
FLOW_PACKAGES,
|
|
||||||
findProducerStep,
|
|
||||||
getAllDbTables,
|
getAllDbTables,
|
||||||
getAllRagCollections,
|
getAllRagCollections,
|
||||||
type SDKFlowStep,
|
type SDKFlowStep,
|
||||||
} from './flow-data'
|
} from './flow-data'
|
||||||
|
import { BetriebOverviewPanel } from './_components/BetriebOverviewPanel'
|
||||||
// =============================================================================
|
import { FlowToolbar } from './_components/FlowToolbar'
|
||||||
// TYPES
|
import { FlowCanvas } from './_components/FlowCanvas'
|
||||||
// =============================================================================
|
import { StepTable } from './_components/StepTable'
|
||||||
|
import { useFlowGraph } from './_components/useFlowGraph'
|
||||||
type PackageFilter = 'alle' | SDKFlowStep['package']
|
import { type PackageFilter } from './_components/helpers'
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 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 & 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() {
|
export default function SDKFlowPage() {
|
||||||
const [selectedStep, setSelectedStep] = useState<SDKFlowStep | null>(null)
|
const [selectedStep, setSelectedStep] = useState<SDKFlowStep | null>(null)
|
||||||
@@ -443,242 +44,12 @@ export default function SDKFlowPage() {
|
|||||||
// Build Nodes
|
// Build Nodes
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
const { nodes: initialNodes, edges: initialEdges } = useMemo(() => {
|
const { nodes: initialNodes, edges: initialEdges } = useFlowGraph(
|
||||||
const nodes: Node[] = []
|
packageFilter,
|
||||||
const edges: Edge[] = []
|
showDb,
|
||||||
|
showRag,
|
||||||
const visibleSteps =
|
selectedStep,
|
||||||
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
|
// React Flow State
|
||||||
@@ -746,274 +117,40 @@ export default function SDKFlowPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Toolbar */}
|
{/* Toolbar */}
|
||||||
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
|
<FlowToolbar
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
packageFilter={packageFilter}
|
||||||
{/* Package Filter */}
|
setPackageFilter={setPackageFilter}
|
||||||
<button
|
setSelectedStep={setSelectedStep}
|
||||||
onClick={() => {
|
showDb={showDb}
|
||||||
setPackageFilter('alle')
|
setShowDb={setShowDb}
|
||||||
setSelectedStep(null)
|
showRag={showRag}
|
||||||
}}
|
setShowRag={setShowRag}
|
||||||
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 */}
|
{/* Flow Canvas + Detail Panel */}
|
||||||
<div className="flex bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden" style={{ height: '700px' }}>
|
<FlowCanvas
|
||||||
{/* Canvas */}
|
nodes={nodes}
|
||||||
<div className="flex-1 relative">
|
edges={edges}
|
||||||
<ReactFlow
|
onNodesChange={onNodesChange}
|
||||||
nodes={nodes}
|
onEdgesChange={onEdgesChange}
|
||||||
edges={edges}
|
onNodeClick={onNodeClick}
|
||||||
onNodesChange={onNodesChange}
|
onPaneClick={onPaneClick}
|
||||||
onEdgesChange={onEdgesChange}
|
packageFilter={packageFilter}
|
||||||
onNodeClick={onNodeClick}
|
selectedStep={selectedStep}
|
||||||
onPaneClick={onPaneClick}
|
setSelectedStep={setSelectedStep}
|
||||||
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 */}
|
{/* Betrieb Fertigstellungsgrad Übersicht */}
|
||||||
{packageFilter === 'betrieb' && <BetriebOverviewPanel />}
|
{packageFilter === 'betrieb' && <BetriebOverviewPanel />}
|
||||||
|
|
||||||
{/* Step Table (below flow) */}
|
{/* Step Table (below flow) */}
|
||||||
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
|
<StepTable
|
||||||
<div className="px-4 py-3 bg-slate-50 border-b">
|
packageFilter={packageFilter}
|
||||||
<h3 className="font-medium text-slate-700">
|
selectedStep={selectedStep}
|
||||||
Alle Steps ({stats.visible}
|
setSelectedStep={setSelectedStep}
|
||||||
{packageFilter !== 'alle' ? ` / ${stats.total}` : ''})
|
visibleCount={stats.visible}
|
||||||
</h3>
|
totalCount={stats.total}
|
||||||
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user