import { useEffect, useMemo, useState } from 'react' import { useNavigate } from 'react-router-dom' import { listInitiativeRoadmapDependencies, listInitiativeRoadmapGraphState } from '../api/roadmap.js' import { ROADMAP_ITEM_TYPE_LABELS } from '../constants/status.js' import { gatePath } from '../utils/routes.js' import { computeGateGraphLayout } from '../plan/gateGraphLayout.js' import { LoadingState } from './LoadingState.jsx' import { ErrorState } from './ErrorState.jsx' import { EmptyState } from './EmptyState.jsx' function edgePath(edge) { const midY = (edge.y1 + edge.y2) / 2 return `M ${edge.x1} ${edge.y1} C ${edge.x1} ${midY}, ${edge.x2} ${midY}, ${edge.x2} ${edge.y2}` } function truncateTitle(title, max = 28) { if (!title || title.length <= max) return title || 'Gate' return `${title.slice(0, max - 1)}…` } export function GateMapView({ initiativeId, items, graphState: graphStateProp }) { const navigate = useNavigate() const [dependencies, setDependencies] = useState([]) const [graphStateLocal, setGraphStateLocal] = useState(null) const graphState = graphStateProp ?? graphStateLocal const [loading, setLoading] = useState(true) const [error, setError] = useState(null) useEffect(() => { if (!initiativeId) { setDependencies([]) if (!graphStateProp) setGraphStateLocal(null) setLoading(false) return undefined } let cancelled = false setLoading(true) setError(null) const loaders = [listInitiativeRoadmapDependencies(initiativeId)] if (!graphStateProp) { loaders.push(listInitiativeRoadmapGraphState(initiativeId)) } Promise.allSettled(loaders).then(([depsResult, graphResult]) => { if (cancelled) return if (depsResult.status === 'fulfilled') { setDependencies(Array.isArray(depsResult.value) ? depsResult.value : []) } else { setError( depsResult.reason?.message || 'Abhängigkeiten konnten nicht geladen werden.', ) } if (!graphStateProp && graphResult?.status === 'fulfilled') { setGraphStateLocal(graphResult.value || null) } else if (!graphStateProp) { setGraphStateLocal(null) } setLoading(false) }) return () => { cancelled = true } }, [initiativeId, graphStateProp]) const layout = useMemo( () => computeGateGraphLayout(items, dependencies), [items, dependencies], ) if (!items.length) { return ( ) } if (loading) { return } if (error) { return } return (

Zielzustände (Graph)

Read-only Übersicht der Gate-Abhängigkeiten — Kanten auf der Gate-Detailseite pflegen, Verify und Kriterien dort ebenfalls.

Voraussetzung Blockiert Bezug
{layout.edges.map((edge) => ( ))} {layout.nodes.map((node) => { const item = node.item const nodeGraph = graphState?.items?.[node.id] const enforceBlocking = graphState?.graph_profile?.enforce_gate_blocking !== false const nodeClass = 'gate-map-view__node gate-map-view__node--' + (item.status || 'planned') + (enforceBlocking && nodeGraph?.blocked ? ' gate-map-view__node--blocked' : '') + (nodeGraph?.ready ? ' gate-map-view__node--ready' : '') return ( navigate(gatePath(node.id))} onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault() navigate(gatePath(node.id)) } }} > {truncateTitle(item.title)} {ROADMAP_ITEM_TYPE_LABELS[item.item_type] || item.item_type || 'Gate'} ) })}
{dependencies.length === 0 && (

Noch keine Kanten — auf der Gate-Detailseite unter „Abhängigkeiten“ anlegen; ohne Kanten gelten Gates nach Reihenfolge.

)}
) }