import { useEffect, useMemo, useState } from 'react' import { Link } from 'react-router-dom' import { gatePath } from '../utils/routes.js' import { listInitiativeRoadmapCriteriaProgress, listInitiativeRoadmapGraphState } from '../api/roadmap.js' import { ROADMAP_ITEM_TYPE_LABELS } from '../constants/status.js' import { StatusBadge } from './StatusBadge.jsx' import { GateGraphStateBadge } from './GateGraphStateBadge.jsx' import { GateFulfillmentBadge } from './GateFulfillmentBadge.jsx' import { EmptyState } from './EmptyState.jsx' import { Modal } from './Modal.jsx' import { RoadmapItemForm } from './RoadmapItemForm.jsx' import { ReorderControls } from './ReorderControls.jsx' import { computeDropPatches, computeMovePatches, sortByOrder } from '../utils/reorder.js' import { useMinWidth } from '../hooks/useMinWidth.js' function formatDate(value) { if (!value) return null try { return new Date(value.includes('T') ? value : `${value}T12:00:00`).toLocaleDateString( 'de-DE', ) } catch { return value } } export function RoadmapPlanSection({ initiativeId, items, canManage, onCreate, onReorder, onDelete, busy, graphState: graphStateProp, }) { const [showCreate, setShowCreate] = useState(false) const [dragItemId, setDragItemId] = useState('') const [dropTargetId, setDropTargetId] = useState('') const [criteriaProgress, setCriteriaProgress] = useState({}) const [graphStateLocal, setGraphStateLocal] = useState(null) const graphState = graphStateProp ?? graphStateLocal const isDesktop = useMinWidth(1024) const canReorder = canManage && typeof onReorder === 'function' useEffect(() => { if (!initiativeId) { setCriteriaProgress({}) if (!graphStateProp) setGraphStateLocal(null) return undefined } let cancelled = false const loaders = [listInitiativeRoadmapCriteriaProgress(initiativeId)] if (!graphStateProp) { loaders.push(listInitiativeRoadmapGraphState(initiativeId)) } Promise.all(loaders) .then((results) => { if (cancelled) return setCriteriaProgress(results[0] || {}) if (!graphStateProp && results[1]) { setGraphStateLocal(results[1] || null) } }) .catch(() => { if (!cancelled) { setCriteriaProgress({}) if (!graphStateProp) setGraphStateLocal(null) } }) return () => { cancelled = true } }, [initiativeId, items, graphStateProp]) const sortedItems = useMemo(() => sortByOrder(items), [items]) async function handleCreateSubmit(payload) { await onCreate(payload) setShowCreate(false) } async function applyPatches(patches) { if (!patches.length) return await onReorder(patches) } async function handleMove(itemId, direction) { const patches = computeMovePatches(sortedItems, itemId, direction) await applyPatches(patches) } function handleDragStart(event, itemId) { if (!canReorder || !isDesktop) return setDragItemId(itemId) event.dataTransfer.effectAllowed = 'move' event.dataTransfer.setData('text/plain', itemId) } function handleDragEnd() { setDragItemId('') setDropTargetId('') } function handleDragOver(event, itemId) { if (!canReorder || !isDesktop || !dragItemId) return event.preventDefault() event.dataTransfer.dropEffect = 'move' setDropTargetId(itemId) } async function handleDrop(event, targetId) { event.preventDefault() if (!canReorder || !isDesktop || !dragItemId) { handleDragEnd() return } const patches = computeDropPatches(sortedItems, dragItemId, targetId) handleDragEnd() await applyPatches(patches) } return (

Zielzustände (Gates)

Überprüfbare Zielpunkte — optional. Reihenfolge per Drag & Drop (Desktop) oder ↑/↓ (Mobile). Bearbeitung und Verify auf der Gate-Detailseite.

{canManage && ( )}
{items.length === 0 && ( )} setShowCreate(false)}> setShowCreate(false)} busy={busy} submitLabel="Anlegen" mode="create" />
) }