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
Fokus-Gates mit Kriterien-Checkliste und eingebetteter Graph-Sicht in Kontrolle; Abnahme-Reihenfolge in Roadmap dokumentiert. Co-authored-by: Cursor <cursoragent@cursor.com>
172 lines
5.6 KiB
JavaScript
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>
|
|
</>
|
|
)
|
|
}
|