import { useCallback, useEffect, useState } from 'react' import { Link } from 'react-router-dom' import { getInitiativePlanIstView, createInitiativePlanSnapshot, } from '../api/roadmap.js' import { gatePath } from '../utils/routes.js' import { CRITERION_STATUS_LABELS, MILESTONE_STATUS_LABELS, ROADMAP_ITEM_TYPE_LABELS, } from '../constants/status.js' import { LoadingState } from './LoadingState.jsx' import { ErrorState } from './ErrorState.jsx' import { Modal } from './Modal.jsx' import { EmptyState } from './EmptyState.jsx' const DIFF_KIND_LABELS = { gate_added: 'Gate neu', gate_removed: 'Gate entfernt', gate_modified: 'Gate geändert', edge_added: 'Kante neu', edge_removed: 'Kante entfernt', criterion_added: 'Kriterium neu', criterion_removed: 'Kriterium entfernt', criterion_modified: 'Kriterium geändert', ist_gate_status: 'Ist-Status Gate', ist_gate_at_risk: 'Gate gefährdet', ist_criterion_waived: 'Kriterium ausgelassen', ist_criterion_deferred: 'Kriterium verschoben', ist_criterion_failed: 'Kriterium fehlgeschlagen', } function formatPct(ratio) { if (ratio == null) return null return `${Math.round(ratio * 100)}%` } export function PlanIstPanel({ initiativeId, canManage = false }) { const [view, setView] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [tab, setTab] = useState('plan') const [busy, setBusy] = useState(false) const [showRevision, setShowRevision] = useState(false) const [revisionReason, setRevisionReason] = useState('') const load = useCallback(async () => { if (!initiativeId) return setLoading(true) setError(null) try { setView(await getInitiativePlanIstView(initiativeId)) } catch (err) { setError(err.message) } finally { setLoading(false) } }, [initiativeId]) useEffect(() => { load() }, [load]) async function handleCreateRevision(e) { e.preventDefault() setBusy(true) setError(null) try { await createInitiativePlanSnapshot(initiativeId, { reason: revisionReason.trim(), }) setRevisionReason('') setShowRevision(false) await load() } catch (err) { setError(err.message) } finally { setBusy(false) } } if (loading) return if (error && !view) return if (!view) return null const planItems = view.plan?.items || [] const istItems = view.ist?.items || [] const diffs = view.diff || [] return (

Plan vs. Ist

Soll-Graph und Ist-Overlay parallel — Abweichungen sichtbar, Plan-Revision mit Audit.

{view.latest_snapshot ? (

Baseline: Revision {view.latest_snapshot.revision_number} {view.latest_snapshot.reason ? ` — ${view.latest_snapshot.reason}` : ''}

) : (

Noch keine Plan-Revision — aktueller Plan = live. Revision festhalten vor größeren Änderungen.

)}
{canManage && ( )}
{error &&

{error}

}
{['plan', 'ist', 'diff'].map((key) => ( ))}
{tab === 'plan' && (
{planItems.length === 0 && ( )}
    {planItems.map((item) => (
  • {item.title}

    {ROADMAP_ITEM_TYPE_LABELS[item.item_type] || item.item_type} {item.target_date ? ` · Ziel ${item.target_date}` : ''}

    {item.goal_description && (

    {item.goal_description}

    )}
  • ))}
{view.plan_source === 'snapshot' && view.current_plan && (

Live-Plan weicht von Revision ab — siehe Diff.

)}
)} {tab === 'ist' && (
{view.ist?.graph_state?.initiative_fulfillment_ratio != null && (

Erfüllungsgrad:{' '} {formatPct(view.ist.graph_state.initiative_fulfillment_ratio)}

)}
    {istItems.map((item) => (
  • {item.title}

    {MILESTONE_STATUS_LABELS[item.status] || item.status} {item.fulfillment_ratio != null ? ` · ${formatPct(item.fulfillment_ratio)} Kriterien` : ''}

    {item.criteria?.length > 0 && (
      {item.criteria.map((crit) => (
    • {crit.title}:{' '} {CRITERION_STATUS_LABELS[crit.status] || crit.status}
    • ))}
    )}
  • ))}
)} {tab === 'diff' && (
{diffs.length === 0 && ( )}
    {diffs.map((entry, index) => (
  • {DIFF_KIND_LABELS[entry.kind] || entry.kind} {entry.title || entry.message}

    {entry.message}

    {entry.item_id && (
    Gate
    )}
  • ))}
)} setShowRevision(false)}>

Speichert Topologie, Kanten und Kriterien-Definitionen als Soll-Baseline — Ist-Status bleibt getrennt.