Kairo-Jinkendo/frontend/src/pages/initiative/InitiativeJourneyPage.jsx
Lars b85a6ec0c7
All checks were successful
Deploy Development / deploy (push) Successful in 47s
Test Suite / pytest-backend (push) Successful in 2m23s
Test Suite / lint-backend (push) Successful in 2s
Test Suite / compose-smoke (push) Has been skipped
Test Suite / k6 /api/health Baseline (push) Successful in 18s
Test Suite / playwright-smoke (push) Successful in 13s
AP1.6b: Journey um Gate-Fortschritt und Ist-Graph erweitern.
Fokus-Gates mit Kriterien-Checkliste und eingebetteter Graph-Sicht in Kontrolle; Abnahme-Reihenfolge in Roadmap dokumentiert.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-11 09:48:14 +02:00

172 lines
5.6 KiB
JavaScript

import { useCallback, useEffect, useState } from 'react'
import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx'
import { listInitiativeJourney } from '../../api/journey.js'
import {
listInitiativeRoadmapCriteriaProgress,
listInitiativeRoadmapGraphState,
listRoadmapItemCriteria,
} from '../../api/roadmap.js'
import { DecisionsSection } from '../../components/DecisionsSection.jsx'
import { ReviewsSection } from '../../components/ReviewsSection.jsx'
import { RecurringSection } from '../../components/RecurringSection.jsx'
import { EvidenceSection } from '../../components/EvidenceSection.jsx'
import { JourneyTimeline } from '../../components/JourneyTimeline.jsx'
import { JourneyGateProgressSection, sortGatesForJourney } from '../../components/JourneyGateProgressSection.jsx'
import { JourneyGateMapSection } from '../../components/JourneyGateMapSection.jsx'
export function InitiativeJourneyPage() {
const ops = useInitiativeOperations()
const { id, capabilities, error, formBusy, roadmapItems } = ops
const [journeyEvents, setJourneyEvents] = useState([])
const [journeyLoading, setJourneyLoading] = useState(true)
const [journeyError, setJourneyError] = useState(null)
const [graphState, setGraphState] = useState(null)
const [criteriaProgress, setCriteriaProgress] = useState({})
const [criteriaByGate, setCriteriaByGate] = useState({})
const [gateMetaLoading, setGateMetaLoading] = useState(true)
const loadJourney = useCallback(async () => {
if (!id) return
setJourneyLoading(true)
setJourneyError(null)
try {
const data = await listInitiativeJourney(id)
setJourneyEvents(data.events || [])
} catch (err) {
setJourneyError(err.message)
} finally {
setJourneyLoading(false)
}
}, [id])
useEffect(() => {
if (!id || !roadmapItems.length) {
setGraphState(null)
setCriteriaProgress({})
setCriteriaByGate({})
setGateMetaLoading(false)
return undefined
}
let cancelled = false
setGateMetaLoading(true)
Promise.all([
listInitiativeRoadmapGraphState(id),
listInitiativeRoadmapCriteriaProgress(id),
])
.then(async ([graph, progress]) => {
if (cancelled) return
setGraphState(graph || null)
setCriteriaProgress(progress || {})
const sortedFocus = sortGatesForJourney(roadmapItems, graph || null)
.slice(0, 5)
.map((item) => item.id)
const criteriaEntries = await Promise.all(
sortedFocus.map((gateId) =>
listRoadmapItemCriteria(gateId)
.then((criteria) => [gateId, Array.isArray(criteria) ? criteria : []])
.catch(() => [gateId, []]),
),
)
if (cancelled) return
setCriteriaByGate(Object.fromEntries(criteriaEntries))
})
.catch(() => {
if (!cancelled) {
setGraphState(null)
setCriteriaProgress({})
setCriteriaByGate({})
}
})
.finally(() => {
if (!cancelled) setGateMetaLoading(false)
})
return () => {
cancelled = true
}
}, [id, roadmapItems])
useEffect(() => {
loadJourney()
}, [loadJourney, ops.decisions, ops.reviews, ops.evidenceItems, ops.actions])
if (!capabilities.has('kairo.initiative.read')) {
return null
}
return (
<>
{error && <p className="error">{error}</p>}
<JourneyGateProgressSection
items={roadmapItems}
graphState={graphState}
criteriaProgress={criteriaProgress}
criteriaByGate={criteriaByGate}
loading={gateMetaLoading}
/>
<JourneyGateMapSection
initiativeId={id}
items={roadmapItems}
graphState={graphState}
loading={gateMetaLoading}
/>
<section className="card journey-timeline-section">
<h2>Journey</h2>
<p className="section-lead muted">
Chronologie aus Entscheidungen, Nachweisen, committeter Arbeit und Zielzuständen Plan/Ist
nachvollziehbar.
</p>
{journeyError && <p className="error">{journeyError}</p>}
{journeyLoading ? (
<p className="muted">Timeline wird geladen</p>
) : (
<JourneyTimeline
events={journeyEvents}
initiativeId={id}
roadmapItems={roadmapItems}
/>
)}
</section>
<div className="initiative-journey-stack">
<DecisionsSection
items={ops.decisions}
canManage={capabilities.has('kairo.decision.manage')}
onCreate={ops.handleCreateDecision}
onUpdateStatus={ops.handleDecisionStatus}
onDelete={ops.handleDeleteDecision}
busy={formBusy}
/>
<ReviewsSection
items={ops.reviews}
canManage={capabilities.has('kairo.review.manage')}
onCreate={ops.handleCreateReview}
onUpdateStatus={ops.handleReviewStatus}
onDelete={ops.handleDeleteReview}
busy={formBusy}
/>
<RecurringSection
items={ops.recurringItems}
canManage={capabilities.has('kairo.recurring.manage')}
onCreate={ops.handleCreateRecurring}
onUpdateStatus={ops.handleRecurringStatus}
onDelete={ops.handleDeleteRecurring}
busy={formBusy}
/>
<EvidenceSection
items={ops.evidenceItems}
roadmapItems={ops.roadmapItems}
canManage={capabilities.has('kairo.evidence.manage')}
onCreate={ops.handleCreateEvidence}
onUpdateStatus={ops.handleEvidenceStatus}
onDelete={ops.handleDeleteEvidence}
busy={formBusy}
/>
</div>
</>
)
}