AP1.6b: Journey um Gate-Fortschritt und Ist-Graph erweitern.
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
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>
This commit is contained in:
parent
a19c6abd26
commit
b85a6ec0c7
|
|
@ -142,6 +142,17 @@ Phase L Validation AP0.10d (laufend PO) parallel
|
||||||
|
|
||||||
## 4. Nächste Pakete (verbindliche Reihenfolge — PO 2026-07-06)
|
## 4. Nächste Pakete (verbindliche Reihenfolge — PO 2026-07-06)
|
||||||
|
|
||||||
|
### 4.0 Abnahme-Reihenfolge (Stand 2026-07-11)
|
||||||
|
|
||||||
|
| # | Paket | Status | Begründung |
|
||||||
|
|---|-------|--------|------------|
|
||||||
|
| 1 | **AP1.6b** Journey Gate-Fortschritt + Ist-Graph | ✓ | ADP-Lücke schließen; Plan/Ist in Kontrolle sichtbar |
|
||||||
|
| 2 | **AP1.9c/d** Cockpit + Kontrolle Polish | offen | Signale auf Portfolio-Kacheln, Filter |
|
||||||
|
| 3 | **AP1.5d** Rekursive Tasks (Rest) | → | Backend + Outline da; ggf. Task-Detail-Route |
|
||||||
|
| 4 | **AP1.8b** Situativer Kontext | deferred | Nach Portfolio-Prio (8a ✓) |
|
||||||
|
| 5 | **AP1.7** Agent Interface | offen | Nach Operating Model tragfähig |
|
||||||
|
| 6 | **AP1.15d** OR-Alternativpfade | deferred | Nur bei Use Case |
|
||||||
|
|
||||||
### AP1.5 — Hierarchie & Ist-Ebene ✓
|
### AP1.5 — Hierarchie & Ist-Ebene ✓
|
||||||
|
|
||||||
**Ziel:** Operativer Plan zwischen Gates — Project, Task, Ausführung-Tab; Gates optional nutzbar.
|
**Ziel:** Operativer Plan zwischen Gates — Project, Task, Ausführung-Tab; Gates optional nutzbar.
|
||||||
|
|
@ -193,7 +204,12 @@ Backlog/Action/Project/Task → RoadmapItem; Decision-Spur; Journey-Timeline.
|
||||||
|
|
||||||
**Nach AP1.5c** — Gates bekommen Bedeutung durch Ist-Bezug (auch auf Project-Ebenen).
|
**Nach AP1.5c** — Gates bekommen Bedeutung durch Ist-Bezug (auch auf Project-Ebenen).
|
||||||
|
|
||||||
**Version:** `0.15.0-ap1.6`
|
| Slice | Scope | Status |
|
||||||
|
|-------|--------|--------|
|
||||||
|
| **6a** | Plan/Ist-Links, Journey-Timeline, Gate-Beiträge | ✓ |
|
||||||
|
| **6b** | Journey: Fokus-Gates + Kriterien (read-only), eingebetteter Ist-Graph | ✓ |
|
||||||
|
|
||||||
|
**Version:** `0.15.0-ap1.6` / `0.15.0-ap1.6b`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# Kairo — Implementation Truth Table v0.1
|
# Kairo — Implementation Truth Table v0.1
|
||||||
|
|
||||||
**Status:** living document — bei jedem AP aktualisieren
|
**Status:** living document — bei jedem AP aktualisieren
|
||||||
**Stand:** 2026-07-10 (nach AP1.13)
|
**Stand:** 2026-07-11 (nach AP1.6b)
|
||||||
**Zweck:** Ehrliche Trennung von **implementiert**, **teilweise**, **nur API/Schema**, **nur Dokumentiert**
|
**Zweck:** Ehrliche Trennung von **implementiert**, **teilweise**, **nur API/Schema**, **nur Dokumentiert**
|
||||||
|
|
||||||
Verhindert, dass Zielbild-Dokumente als Ist-Stand gelesen werden.
|
Verhindert, dass Zielbild-Dokumente als Ist-Stand gelesen werden.
|
||||||
|
|
@ -87,6 +87,7 @@ Verhindert, dass Zielbild-Dokumente als Ist-Stand gelesen werden.
|
||||||
| Workspace (Portfolio aller Initiativen) | ✓ | Portfolio-Widget |
|
| Workspace (Portfolio aller Initiativen) | ✓ | Portfolio-Widget |
|
||||||
| Initiative-Übersicht (operative Steuerung) | ✓ | AP1.2c |
|
| Initiative-Übersicht (operative Steuerung) | ✓ | AP1.2c |
|
||||||
| Initiative Unterseiten (Plan, Inbox, …) | ✓ | Shell + Pflege verschoben |
|
| Initiative Unterseiten (Plan, Inbox, …) | ✓ | Shell + Pflege verschoben |
|
||||||
|
| Journey (Kontrolle / Initiative) | ◐ | AP1.6b: Fokus-Gates + Kriterien read-only, Ist-Graph eingebettet |
|
||||||
| Next-Action-Widget konfigurierbar (Workspace + Initiative) | ✓ | scope portfolio/initiative |
|
| Next-Action-Widget konfigurierbar (Workspace + Initiative) | ✓ | scope portfolio/initiative |
|
||||||
| Action Detail-Route | ✓ | `/initiatives/:id/actions/:actionId` |
|
| Action Detail-Route | ✓ | `/initiatives/:id/actions/:actionId` |
|
||||||
| Action Detail-Seite | ✗ | |
|
| Action Detail-Seite | ✗ | |
|
||||||
|
|
@ -104,7 +105,7 @@ Verhindert, dass Zielbild-Dokumente als Ist-Stand gelesen werden.
|
||||||
| Zieltermin | ◐ | `target_date` |
|
| Zieltermin | ◐ | `target_date` |
|
||||||
| Abhängigkeiten | ◐ | API; Graph Engine AP1.4d |
|
| Abhängigkeiten | ◐ | API; Graph Engine AP1.4d |
|
||||||
| parallel vs. sequenziell | ◐ | `sequencing_mode` (deprecated); Graph AP1.4d |
|
| parallel vs. sequenziell | ◐ | `sequencing_mode` (deprecated); Graph AP1.4d |
|
||||||
| Plan-Graph vs. Ist-Overlay | ✗ | AP1.13: ein Graph + Status am Knoten — Plan/Ist vermischt |
|
| Plan-Graph vs. Ist-Overlay | ◐ | AP1.13 Designer + AP1.6b Journey Ist-Graph (read-only); Plan/Ist-Diff AP1.14 |
|
||||||
| Zielzustands-Designer (Joint/Parallel) | ◐ | AP1.15a/b Designer; AP1.15c parallel/optional in Engine+Designer; OR deferred |
|
| Zielzustands-Designer (Joint/Parallel) | ◐ | AP1.15a/b Designer; AP1.15c parallel/optional in Engine+Designer; OR deferred |
|
||||||
| Plan-Revision / Graph-Historie | ◐ | AP1.14 Plan-Snapshots |
|
| Plan-Revision / Graph-Historie | ◐ | AP1.14 Plan-Snapshots |
|
||||||
| Verify vor `reached` | ◐ | Kriterienplan + gate_override |
|
| Verify vor `reached` | ◐ | Kriterienplan + gate_override |
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ function truncateTitle(title, max = 28) {
|
||||||
return `${title.slice(0, max - 1)}…`
|
return `${title.slice(0, max - 1)}…`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GateMapView({ initiativeId, items, graphState: graphStateProp }) {
|
export function GateMapView({ initiativeId, items, graphState: graphStateProp, embedded = false }) {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [dependencies, setDependencies] = useState([])
|
const [dependencies, setDependencies] = useState([])
|
||||||
const [graphStateLocal, setGraphStateLocal] = useState(null)
|
const [graphStateLocal, setGraphStateLocal] = useState(null)
|
||||||
|
|
@ -80,17 +80,22 @@ export function GateMapView({ initiativeId, items, graphState: graphStateProp })
|
||||||
return <ErrorState message={error} />
|
return <ErrorState message={error} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Wrapper = embedded ? 'div' : 'section'
|
||||||
|
const wrapperClass = embedded ? 'gate-map-view gate-map-view--embedded' : 'card gate-map-view'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="card gate-map-view">
|
<Wrapper className={wrapperClass}>
|
||||||
<div className="section-header">
|
{!embedded && (
|
||||||
<div>
|
<div className="section-header">
|
||||||
<h2>Zielzustände (Graph)</h2>
|
<div>
|
||||||
<p className="section-lead muted">
|
<h2>Zielzustände (Graph)</h2>
|
||||||
Read-only Übersicht der Gate-Abhängigkeiten — im Designer modellieren oder auf der
|
<p className="section-lead muted">
|
||||||
Gate-Detailseite Kanten pflegen; Verify und Kriterien dort.
|
Read-only Übersicht der Gate-Abhängigkeiten — im Designer modellieren oder auf der
|
||||||
</p>
|
Gate-Detailseite Kanten pflegen; Verify und Kriterien dort.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
<div className="gate-map-view__legend muted">
|
<div className="gate-map-view__legend muted">
|
||||||
<span className="gate-map-view__legend-item">
|
<span className="gate-map-view__legend-item">
|
||||||
|
|
@ -209,6 +214,6 @@ export function GateMapView({ initiativeId, items, graphState: graphStateProp })
|
||||||
„Abhängigkeiten“ anlegen; ohne Kanten gelten Gates nach Reihenfolge.
|
„Abhängigkeiten“ anlegen; ohne Kanten gelten Gates nach Reihenfolge.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</section>
|
</Wrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
25
frontend/src/components/JourneyGateMapSection.jsx
Normal file
25
frontend/src/components/JourneyGateMapSection.jsx
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { GateMapView } from './GateMapView.jsx'
|
||||||
|
|
||||||
|
export function JourneyGateMapSection({ initiativeId, items, graphState, loading }) {
|
||||||
|
if (!items.length) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="card journey-gate-map">
|
||||||
|
<h2>Zielzustands-Graph (Ist)</h2>
|
||||||
|
<p className="section-lead muted">
|
||||||
|
Baum der Gate-Abhängigkeiten mit Ist-Status — keine Planungsbearbeitung (Kontrolle /
|
||||||
|
Journey).
|
||||||
|
</p>
|
||||||
|
{loading ? (
|
||||||
|
<p className="muted">Graph wird geladen …</p>
|
||||||
|
) : (
|
||||||
|
<GateMapView
|
||||||
|
initiativeId={initiativeId}
|
||||||
|
items={items}
|
||||||
|
graphState={graphState}
|
||||||
|
embedded
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
142
frontend/src/components/JourneyGateProgressSection.jsx
Normal file
142
frontend/src/components/JourneyGateProgressSection.jsx
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { gateTitleById } from './GateSelect.jsx'
|
||||||
|
import { gatePath } from '../utils/routes.js'
|
||||||
|
import { CRITERION_STATUS_LABELS } from '../constants/status.js'
|
||||||
|
import { StatusBadge } from './StatusBadge.jsx'
|
||||||
|
|
||||||
|
export function GateSteeringHint({ graphState, itemId, siblingItems = [] }) {
|
||||||
|
if (!graphState?.items?.[itemId]) return null
|
||||||
|
|
||||||
|
const itemState = graphState.items[itemId]
|
||||||
|
const enforceBlocking = graphState.graph_profile?.enforce_gate_blocking !== false
|
||||||
|
const blockedBy = itemState.blocked_by || []
|
||||||
|
const wouldBlock = itemState.would_block ?? itemState.blocked
|
||||||
|
|
||||||
|
if (itemState.ready) {
|
||||||
|
return <span className="status-pill status-pill--ready">Bereit</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wouldBlock && blockedBy.length > 0) {
|
||||||
|
const blockerTitle =
|
||||||
|
blockedBy.length === 1 ? gateTitleById(siblingItems, blockedBy[0]) : null
|
||||||
|
const title =
|
||||||
|
blockerTitle != null
|
||||||
|
? `Wartet auf: ${blockerTitle}`
|
||||||
|
: `Wartet auf ${blockedBy.length} Voraussetzungen`
|
||||||
|
const className = enforceBlocking
|
||||||
|
? 'status-pill status-pill--blocked'
|
||||||
|
: 'status-pill status-pill--wait'
|
||||||
|
const label = enforceBlocking ? 'Blockiert' : 'Wartet'
|
||||||
|
return (
|
||||||
|
<span className={className} title={title}>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortGatesForJourney(items, graphState) {
|
||||||
|
const terminal = new Set(['reached', 'moved', 'discarded'])
|
||||||
|
const open = items.filter((item) => !terminal.has(item.status))
|
||||||
|
return [...open].sort((a, b) => {
|
||||||
|
const aState = graphState?.items?.[a.id]
|
||||||
|
const bState = graphState?.items?.[b.id]
|
||||||
|
const aReady = aState?.ready ? 0 : 1
|
||||||
|
const bReady = bState?.ready ? 0 : 1
|
||||||
|
if (aReady !== bReady) return aReady - bReady
|
||||||
|
const aWait = aState?.would_block || aState?.blocked ? 1 : 0
|
||||||
|
const bWait = bState?.would_block || bState?.blocked ? 1 : 0
|
||||||
|
if (aWait !== bWait) return aWait - bWait
|
||||||
|
return (a.sort_order ?? 0) - (b.sort_order ?? 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export { sortGatesForJourney }
|
||||||
|
|
||||||
|
export function JourneyGateProgressSection({
|
||||||
|
items = [],
|
||||||
|
graphState,
|
||||||
|
criteriaProgress = {},
|
||||||
|
criteriaByGate = {},
|
||||||
|
loading = false,
|
||||||
|
}) {
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<section className="card journey-gate-progress">
|
||||||
|
<p className="muted">Zielzustands-Fortschritt wird geladen …</p>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!items.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const focusGates = sortGatesForJourney(items, graphState).slice(0, 5)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="card journey-gate-progress">
|
||||||
|
<h2>Zielzustands-Fortschritt</h2>
|
||||||
|
<p className="section-lead muted">
|
||||||
|
Nächste Gates und Kriterien im Ist — Schritt für Schritt bis Verify. Vollständige Pflege
|
||||||
|
auf der Gate-Detailseite.
|
||||||
|
</p>
|
||||||
|
<ul className="item-list journey-gate-progress-list">
|
||||||
|
{focusGates.map((gate) => {
|
||||||
|
const progress = criteriaProgress[gate.id]
|
||||||
|
const criteria = criteriaByGate[gate.id] || []
|
||||||
|
const openCriteria = criteria.filter(
|
||||||
|
(c) => !['satisfied', 'waived', 'deferred'].includes(c.status),
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<li key={gate.id} className="list-item card-list-item journey-gate-progress-item">
|
||||||
|
<div className="list-item-main">
|
||||||
|
<div className="journey-gate-progress-item__head">
|
||||||
|
<Link to={gatePath(gate.id)} className="link-button">
|
||||||
|
<strong>{gate.title}</strong>
|
||||||
|
</Link>
|
||||||
|
<StatusBadge kind="milestone" status={gate.status} />
|
||||||
|
<GateSteeringHint
|
||||||
|
graphState={graphState}
|
||||||
|
itemId={gate.id}
|
||||||
|
siblingItems={items}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{progress?.total > 0 && (
|
||||||
|
<p className="muted list-item-sub">
|
||||||
|
Kriterien: {progress.closed}/{progress.total} erfüllt
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{criteria.length > 0 && (
|
||||||
|
<ol className="journey-gate-criteria-list">
|
||||||
|
{criteria.map((criterion) => (
|
||||||
|
<li
|
||||||
|
key={criterion.id}
|
||||||
|
className={
|
||||||
|
'journey-gate-criterion' +
|
||||||
|
(['satisfied', 'waived', 'deferred'].includes(criterion.status)
|
||||||
|
? ' journey-gate-criterion--done'
|
||||||
|
: '')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="journey-gate-criterion__title">{criterion.title}</span>
|
||||||
|
<span className="muted journey-gate-criterion__status">
|
||||||
|
{CRITERION_STATUS_LABELS[criterion.status] || criterion.status}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
)}
|
||||||
|
{openCriteria.length === 0 && criteria.length > 0 && gate.status !== 'reached' && (
|
||||||
|
<p className="muted list-item-sub">Alle Kriterien bearbeitet — Verify prüfen.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
|
import { actionPath, gatePath } from '../utils/routes.js'
|
||||||
|
|
||||||
const KIND_LABELS = {
|
const KIND_LABELS = {
|
||||||
decision: 'Entscheidung',
|
decision: 'Entscheidung',
|
||||||
|
|
@ -40,26 +41,17 @@ export function JourneyTimeline({ events, initiativeId, roadmapItems = [] }) {
|
||||||
{event.summary && <p className="list-item-desc muted">{event.summary}</p>}
|
{event.summary && <p className="list-item-desc muted">{event.summary}</p>}
|
||||||
<div className="journey-event-links">
|
<div className="journey-event-links">
|
||||||
{event.roadmap_item_id && gateTitles[event.roadmap_item_id] && (
|
{event.roadmap_item_id && gateTitles[event.roadmap_item_id] && (
|
||||||
<Link
|
<Link to={gatePath(event.roadmap_item_id)} className="link-button">
|
||||||
to={`/initiatives/${initiativeId}/plan/items/${event.roadmap_item_id}`}
|
|
||||||
className="link-button"
|
|
||||||
>
|
|
||||||
Gate: {gateTitles[event.roadmap_item_id]}
|
Gate: {gateTitles[event.roadmap_item_id]}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{event.kind === 'action' && (
|
{event.kind === 'action' && (
|
||||||
<Link
|
<Link to={actionPath(event.entity_id)} className="link-button">
|
||||||
to={`/initiatives/${initiativeId}/actions/${event.entity_id}`}
|
|
||||||
className="link-button"
|
|
||||||
>
|
|
||||||
Arbeitspaket öffnen
|
Arbeitspaket öffnen
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{event.kind === 'gate' && (
|
{event.kind === 'gate' && (
|
||||||
<Link
|
<Link to={gatePath(event.entity_id)} className="link-button">
|
||||||
to={`/initiatives/${initiativeId}/plan/items/${event.entity_id}`}
|
|
||||||
className="link-button"
|
|
||||||
>
|
|
||||||
Gate öffnen
|
Gate öffnen
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,18 @@
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx'
|
import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx'
|
||||||
import { listInitiativeJourney } from '../../api/journey.js'
|
import { listInitiativeJourney } from '../../api/journey.js'
|
||||||
|
import {
|
||||||
|
listInitiativeRoadmapCriteriaProgress,
|
||||||
|
listInitiativeRoadmapGraphState,
|
||||||
|
listRoadmapItemCriteria,
|
||||||
|
} from '../../api/roadmap.js'
|
||||||
import { DecisionsSection } from '../../components/DecisionsSection.jsx'
|
import { DecisionsSection } from '../../components/DecisionsSection.jsx'
|
||||||
import { ReviewsSection } from '../../components/ReviewsSection.jsx'
|
import { ReviewsSection } from '../../components/ReviewsSection.jsx'
|
||||||
import { RecurringSection } from '../../components/RecurringSection.jsx'
|
import { RecurringSection } from '../../components/RecurringSection.jsx'
|
||||||
import { EvidenceSection } from '../../components/EvidenceSection.jsx'
|
import { EvidenceSection } from '../../components/EvidenceSection.jsx'
|
||||||
import { JourneyTimeline } from '../../components/JourneyTimeline.jsx'
|
import { JourneyTimeline } from '../../components/JourneyTimeline.jsx'
|
||||||
|
import { JourneyGateProgressSection, sortGatesForJourney } from '../../components/JourneyGateProgressSection.jsx'
|
||||||
|
import { JourneyGateMapSection } from '../../components/JourneyGateMapSection.jsx'
|
||||||
|
|
||||||
export function InitiativeJourneyPage() {
|
export function InitiativeJourneyPage() {
|
||||||
const ops = useInitiativeOperations()
|
const ops = useInitiativeOperations()
|
||||||
|
|
@ -13,6 +20,10 @@ export function InitiativeJourneyPage() {
|
||||||
const [journeyEvents, setJourneyEvents] = useState([])
|
const [journeyEvents, setJourneyEvents] = useState([])
|
||||||
const [journeyLoading, setJourneyLoading] = useState(true)
|
const [journeyLoading, setJourneyLoading] = useState(true)
|
||||||
const [journeyError, setJourneyError] = useState(null)
|
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 () => {
|
const loadJourney = useCallback(async () => {
|
||||||
if (!id) return
|
if (!id) return
|
||||||
|
|
@ -28,6 +39,53 @@ export function InitiativeJourneyPage() {
|
||||||
}
|
}
|
||||||
}, [id])
|
}, [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(() => {
|
useEffect(() => {
|
||||||
loadJourney()
|
loadJourney()
|
||||||
}, [loadJourney, ops.decisions, ops.reviews, ops.evidenceItems, ops.actions])
|
}, [loadJourney, ops.decisions, ops.reviews, ops.evidenceItems, ops.actions])
|
||||||
|
|
@ -40,6 +98,21 @@ export function InitiativeJourneyPage() {
|
||||||
<>
|
<>
|
||||||
{error && <p className="error">{error}</p>}
|
{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">
|
<section className="card journey-timeline-section">
|
||||||
<h2>Journey</h2>
|
<h2>Journey</h2>
|
||||||
<p className="section-lead muted">
|
<p className="section-lead muted">
|
||||||
|
|
|
||||||
|
|
@ -1255,6 +1255,52 @@
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.journey-gate-progress-list {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journey-gate-progress-item__head {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journey-gate-criteria-list {
|
||||||
|
margin: 0.5rem 0 0;
|
||||||
|
padding-left: 1.25rem;
|
||||||
|
display: grid;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journey-gate-criterion {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.35rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journey-gate-criterion--done {
|
||||||
|
opacity: 0.72;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journey-gate-criterion__title {
|
||||||
|
flex: 1 1 12rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journey-gate-criterion__status {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journey-gate-map .gate-map-view--embedded {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journey-gate-map .gate-map-view--embedded > h2,
|
||||||
|
.journey-gate-map .gate-map-view--embedded > .section-lead {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.next-action-embedded {
|
.next-action-embedded {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
@ -1850,6 +1896,11 @@
|
||||||
color: #047857;
|
color: #047857;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-pill--wait {
|
||||||
|
background: #fffbeb;
|
||||||
|
color: #b45309;
|
||||||
|
}
|
||||||
|
|
||||||
.status-pill--blocked {
|
.status-pill--blocked {
|
||||||
background: #fef2f2;
|
background: #fef2f2;
|
||||||
color: #b91c1c;
|
color: #b91c1c;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user