From 2b818c6fb3565f6190ecf6d3864f46c895f5f05e Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com> Date: Tue, 14 Apr 2026 22:49:33 +0200 Subject: [PATCH] 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) --- .../_components/BetriebOverviewPanel.tsx | 74 ++ .../sdk/sdk-flow/_components/DetailPanel.tsx | 257 +++++ .../sdk/sdk-flow/_components/FlowCanvas.tsx | 142 +++ .../sdk/sdk-flow/_components/FlowToolbar.tsx | 93 ++ .../sdk/sdk-flow/_components/StepTable.tsx | 114 +++ .../app/sdk/sdk-flow/_components/helpers.ts | 58 ++ .../sdk/sdk-flow/_components/useFlowGraph.tsx | 258 +++++ admin-compliance/app/sdk/sdk-flow/page.tsx | 943 +----------------- 8 files changed, 1036 insertions(+), 903 deletions(-) create mode 100644 admin-compliance/app/sdk/sdk-flow/_components/BetriebOverviewPanel.tsx create mode 100644 admin-compliance/app/sdk/sdk-flow/_components/DetailPanel.tsx create mode 100644 admin-compliance/app/sdk/sdk-flow/_components/FlowCanvas.tsx create mode 100644 admin-compliance/app/sdk/sdk-flow/_components/FlowToolbar.tsx create mode 100644 admin-compliance/app/sdk/sdk-flow/_components/StepTable.tsx create mode 100644 admin-compliance/app/sdk/sdk-flow/_components/helpers.ts create mode 100644 admin-compliance/app/sdk/sdk-flow/_components/useFlowGraph.tsx diff --git a/admin-compliance/app/sdk/sdk-flow/_components/BetriebOverviewPanel.tsx b/admin-compliance/app/sdk/sdk-flow/_components/BetriebOverviewPanel.tsx new file mode 100644 index 0000000..52df682 --- /dev/null +++ b/admin-compliance/app/sdk/sdk-flow/_components/BetriebOverviewPanel.tsx @@ -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 ( +
+
+
+

+ ⚙️ Betrieb — Fertigstellungsgrad +

+

+ {betriebWithCompletion.length} Module bewertet · Academy & Training ausgeschlossen +

+
+
+
+ {avg}% +
+
Ø Fertigstellung
+
+
+ +
+ {betriebWithCompletion.map(step => { + const pct = step.completion ?? 0 + const color = completionColor(pct) + return ( +
+
+
+ {step.name} + + {completionLabel(pct)} + +
+ {pct}% +
+
+
+
+
+ {step.checkpointId && ( + {step.checkpointId} + )} + {step.dbTables.length > 0 ? `DB: ${step.dbTables.length} Tabellen` : 'Kein DB-Backend'} + {step.ragCollections.length > 0 && ( + RAG: {step.ragCollections.length} + )} +
+
+ ) + })} +
+
+ ) +} diff --git a/admin-compliance/app/sdk/sdk-flow/_components/DetailPanel.tsx b/admin-compliance/app/sdk/sdk-flow/_components/DetailPanel.tsx new file mode 100644 index 0000000..65395bd --- /dev/null +++ b/admin-compliance/app/sdk/sdk-flow/_components/DetailPanel.tsx @@ -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 ( +
+
+
+

{step.name}

+ +
+
+ + {pkg.icon} {pkg.name} + + seq {step.seq} + {step.isOptional && ( + + Optional + + )} +
+
+ +
+ {/* Fertigstellungsgrad */} + {step.completion !== undefined && ( +
+
+ + Fertigstellungsgrad + + + {step.completion}% + +
+
+
+
+
+ {completionLabel(step.completion)} +
+
+ )} + + {/* Beschreibung */} +
+

{step.description}

+

{step.descriptionLong}

+ {step.legalBasis && ( +
+ Rechtsgrundlage: {step.legalBasis} +
+ )} +
+ + {/* Inputs */} + {step.inputs.length > 0 && ( +
+

+ Inputs (SDKState) +

+
+ {step.inputs.map(input => { + const producer = findProducerStep(input) + return ( +
+ {input} + {producer && ( + + ← {producer.nameShort} + + )} +
+ ) + })} +
+
+ )} + + {/* Outputs */} + {step.outputs.length > 0 && ( +
+

+ Outputs (SDKState) +

+
+ {step.outputs.map(output => ( +
+ {output} +
+ ))} +
+
+ )} + + {/* DB Tables */} + {step.dbTables.length > 0 && ( +
+

+ DB-Tabellen +

+
+ {step.dbTables.map(table => ( +
+ {table} + {step.dbMode} +
+ ))} +
+
+ )} + + {/* RAG Collections */} + {step.ragCollections.length > 0 && ( +
+

+ RAG-Collections +

+
+ {step.ragCollections.map(rag => ( +
+ {rag} +
+ ))} + {step.ragPurpose && ( +

{step.ragPurpose}

+ )} +
+
+ )} + + {/* Checkpoint */} + {step.checkpointId && ( +
+

+ Checkpoint +

+
+
+ + {step.checkpointId} + + + {step.checkpointType} + +
+ {step.checkpointReviewer && step.checkpointReviewer !== 'NONE' && ( +
+ Reviewer: {step.checkpointReviewer} +
+ )} +
+
+ )} + + {/* Generated Docs */} + {step.generates && step.generates.length > 0 && ( +
+

+ Generierte Dokumente +

+
+ {step.generates.map(doc => ( +
+ {doc} +
+ ))} +
+
+ )} + + {/* Prerequisites */} + {step.prerequisiteSteps.length > 0 && ( +
+

+ Voraussetzungen +

+
+ {step.prerequisiteSteps.map(preId => { + const preStep = SDK_FLOW_STEPS.find(s => s.id === preId) + return ( +
+ {preStep?.name || preId} +
+ ) + })} +
+
+ )} + + {/* Open in SDK */} + +
+
+ ) +} diff --git a/admin-compliance/app/sdk/sdk-flow/_components/FlowCanvas.tsx b/admin-compliance/app/sdk/sdk-flow/_components/FlowCanvas.tsx new file mode 100644 index 0000000..941a582 --- /dev/null +++ b/admin-compliance/app/sdk/sdk-flow/_components/FlowCanvas.tsx @@ -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 ( +
+ {/* Canvas */} +
+ + + { + 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)" + /> + + + {/* Legende */} + +
Legende
+
+ {PACKAGE_ORDER.map(pkgId => { + const pkg = FLOW_PACKAGES[pkgId] + return ( +
+ + {pkg.name} +
+ ) + })} +
+
+ + DB-Tabelle +
+
+ + RAG-Collection +
+
+
+
* = REQUIRED
+
~ = RECOMMENDED
+
--- = gestrichelte Border: Optional
+
+
+
+ + {/* Package Headers */} + {packageFilter === 'alle' && ( + + {PACKAGE_ORDER.map((pkgId) => { + const pkg = FLOW_PACKAGES[pkgId] + return ( +
+ {pkg.icon} {pkg.name} +
+ ) + })} +
+ )} +
+
+ + {/* Detail Panel */} + {selectedStep && ( + setSelectedStep(null)} + /> + )} +
+ ) +} diff --git a/admin-compliance/app/sdk/sdk-flow/_components/FlowToolbar.tsx b/admin-compliance/app/sdk/sdk-flow/_components/FlowToolbar.tsx new file mode 100644 index 0000000..8b751f3 --- /dev/null +++ b/admin-compliance/app/sdk/sdk-flow/_components/FlowToolbar.tsx @@ -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 ( +
+
+ {/* Package Filter */} + + {PACKAGE_ORDER.map(pkgId => { + const pkg = FLOW_PACKAGES[pkgId] + const count = SDK_FLOW_STEPS.filter(s => s.package === pkgId).length + return ( + + ) + })} + + {/* Separator */} +
+ + {/* Toggles */} + + +
+
+ ) +} diff --git a/admin-compliance/app/sdk/sdk-flow/_components/StepTable.tsx b/admin-compliance/app/sdk/sdk-flow/_components/StepTable.tsx new file mode 100644 index 0000000..dba4c83 --- /dev/null +++ b/admin-compliance/app/sdk/sdk-flow/_components/StepTable.tsx @@ -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 ( +
+
+

+ Alle Steps ({visibleCount} + {packageFilter !== 'alle' ? ` / ${totalCount}` : ''}) +

+
+
+ {SDK_FLOW_STEPS.filter( + s => packageFilter === 'alle' || s.package === packageFilter + ).map(step => { + const pkg = FLOW_PACKAGES[step.package] + return ( + + ) + })} +
+
+ ) +} diff --git a/admin-compliance/app/sdk/sdk-flow/_components/helpers.ts b/admin-compliance/app/sdk/sdk-flow/_components/helpers.ts new file mode 100644 index 0000000..ff6b099 --- /dev/null +++ b/admin-compliance/app/sdk/sdk-flow/_components/helpers.ts @@ -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 = { + 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'] diff --git a/admin-compliance/app/sdk/sdk-flow/_components/useFlowGraph.tsx b/admin-compliance/app/sdk/sdk-flow/_components/useFlowGraph.tsx new file mode 100644 index 0000000..46315b9 --- /dev/null +++ b/admin-compliance/app/sdk/sdk-flow/_components/useFlowGraph.tsx @@ -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: ( +
+
+ {step.nameShort} +
+
+ {step.checkpointId || ''} + {badge} +
+ {step.completion !== undefined && ( +
+
+ {step.completion}% +
+
+
+
+
+ )} +
+ ), + }, + 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() + 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: ( +
+
DB
+
{table}
+
+ ), + }, + 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() + 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: ( +
+
RAG
+
+ {collection.replace('bp_', '')} +
+
+ ), + }, + 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]) +} diff --git a/admin-compliance/app/sdk/sdk-flow/page.tsx b/admin-compliance/app/sdk/sdk-flow/page.tsx index 654ab1e..7585ce7 100644 --- a/admin-compliance/app/sdk/sdk-flow/page.tsx +++ b/admin-compliance/app/sdk/sdk-flow/page.tsx @@ -11,424 +11,25 @@ */ import { useCallback, useState, useMemo, useEffect } from 'react' -import ReactFlow, { +import { 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 = { - 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 ( -
-
-
-

- ⚙️ Betrieb — Fertigstellungsgrad -

-

- {betriebWithCompletion.length} Module bewertet · Academy & Training ausgeschlossen -

-
-
-
- {avg}% -
-
Ø Fertigstellung
-
-
- -
- {betriebWithCompletion.map(step => { - const pct = step.completion ?? 0 - const color = completionColor(pct) - return ( -
-
-
- {step.name} - - {completionLabel(pct)} - -
- {pct}% -
-
-
-
-
- {step.checkpointId && ( - {step.checkpointId} - )} - {step.dbTables.length > 0 ? `DB: ${step.dbTables.length} Tabellen` : 'Kein DB-Backend'} - {step.ragCollections.length > 0 && ( - RAG: {step.ragCollections.length} - )} -
-
- ) - })} -
-
- ) -} - -// ============================================================================= -// DETAIL PANEL -// ============================================================================= - -function DetailPanel({ - step, - onClose, -}: { - step: SDKFlowStep - onClose: () => void -}) { - const pkg = FLOW_PACKAGES[step.package] - const baseUrl = 'https://macmini:3007' - - return ( -
-
-
-

{step.name}

- -
-
- - {pkg.icon} {pkg.name} - - seq {step.seq} - {step.isOptional && ( - - Optional - - )} -
-
- -
- {/* Fertigstellungsgrad */} - {step.completion !== undefined && ( -
-
- - Fertigstellungsgrad - - - {step.completion}% - -
-
-
-
-
- {completionLabel(step.completion)} -
-
- )} - - {/* Beschreibung */} -
-

{step.description}

-

{step.descriptionLong}

- {step.legalBasis && ( -
- Rechtsgrundlage: {step.legalBasis} -
- )} -
- - {/* Inputs */} - {step.inputs.length > 0 && ( -
-

- Inputs (SDKState) -

-
- {step.inputs.map(input => { - const producer = findProducerStep(input) - return ( -
- {input} - {producer && ( - - ← {producer.nameShort} - - )} -
- ) - })} -
-
- )} - - {/* Outputs */} - {step.outputs.length > 0 && ( -
-

- Outputs (SDKState) -

-
- {step.outputs.map(output => ( -
- {output} -
- ))} -
-
- )} - - {/* DB Tables */} - {step.dbTables.length > 0 && ( -
-

- DB-Tabellen -

-
- {step.dbTables.map(table => ( -
- {table} - {step.dbMode} -
- ))} -
-
- )} - - {/* RAG Collections */} - {step.ragCollections.length > 0 && ( -
-

- RAG-Collections -

-
- {step.ragCollections.map(rag => ( -
- {rag} -
- ))} - {step.ragPurpose && ( -

{step.ragPurpose}

- )} -
-
- )} - - {/* Checkpoint */} - {step.checkpointId && ( -
-

- Checkpoint -

-
-
- - {step.checkpointId} - - - {step.checkpointType} - -
- {step.checkpointReviewer && step.checkpointReviewer !== 'NONE' && ( -
- Reviewer: {step.checkpointReviewer} -
- )} -
-
- )} - - {/* Generated Docs */} - {step.generates && step.generates.length > 0 && ( -
-

- Generierte Dokumente -

-
- {step.generates.map(doc => ( -
- {doc} -
- ))} -
-
- )} - - {/* Prerequisites */} - {step.prerequisiteSteps.length > 0 && ( -
-

- Voraussetzungen -

-
- {step.prerequisiteSteps.map(preId => { - const preStep = SDK_FLOW_STEPS.find(s => s.id === preId) - return ( -
- {preStep?.name || preId} -
- ) - })} -
-
- )} - - {/* Open in SDK */} - -
-
- ) -} - -// ============================================================================= -// MAIN COMPONENT -// ============================================================================= +import { BetriebOverviewPanel } from './_components/BetriebOverviewPanel' +import { FlowToolbar } from './_components/FlowToolbar' +import { FlowCanvas } from './_components/FlowCanvas' +import { StepTable } from './_components/StepTable' +import { useFlowGraph } from './_components/useFlowGraph' +import { type PackageFilter } from './_components/helpers' export default function SDKFlowPage() { const [selectedStep, setSelectedStep] = useState(null) @@ -443,242 +44,12 @@ export default function SDKFlowPage() { // 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: ( -
-
- {step.nameShort} -
-
- {step.checkpointId || ''} - {badge} -
- {step.completion !== undefined && ( -
-
- {step.completion}% -
-
-
-
-
- )} -
- ), - }, - 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() - 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: ( -
-
DB
-
{table}
-
- ), - }, - 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() - 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: ( -
-
RAG
-
- {collection.replace('bp_', '')} -
-
- ), - }, - 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]) + const { nodes: initialNodes, edges: initialEdges } = useFlowGraph( + packageFilter, + showDb, + showRag, + selectedStep, + ) // ========================================================================= // React Flow State @@ -746,274 +117,40 @@ export default function SDKFlowPage() {
{/* Toolbar */} -
-
- {/* Package Filter */} - - {PACKAGE_ORDER.map(pkgId => { - const pkg = FLOW_PACKAGES[pkgId] - const count = SDK_FLOW_STEPS.filter(s => s.package === pkgId).length - return ( - - ) - })} - - {/* Separator */} -
- - {/* Toggles */} - - -
-
+ {/* Flow Canvas + Detail Panel */} -
- {/* Canvas */} -
- - - { - 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)" - /> - - - {/* Legende */} - -
Legende
-
- {PACKAGE_ORDER.map(pkgId => { - const pkg = FLOW_PACKAGES[pkgId] - return ( -
- - {pkg.name} -
- ) - })} -
-
- - DB-Tabelle -
-
- - RAG-Collection -
-
-
-
* = REQUIRED
-
~ = RECOMMENDED
-
--- = gestrichelte Border: Optional
-
-
-
- - {/* Package Headers */} - {packageFilter === 'alle' && ( - - {PACKAGE_ORDER.map((pkgId) => { - const pkg = FLOW_PACKAGES[pkgId] - return ( -
- {pkg.icon} {pkg.name} -
- ) - })} -
- )} -
-
- - {/* Detail Panel */} - {selectedStep && ( - setSelectedStep(null)} - /> - )} -
+ {/* Betrieb Fertigstellungsgrad Übersicht */} {packageFilter === 'betrieb' && } {/* Step Table (below flow) */} -
-
-

- Alle Steps ({stats.visible} - {packageFilter !== 'alle' ? ` / ${stats.total}` : ''}) -

-
-
- {SDK_FLOW_STEPS.filter( - s => packageFilter === 'alle' || s.package === packageFilter - ).map(step => { - const pkg = FLOW_PACKAGES[step.package] - return ( - - ) - })} -
-
+
) }