AP1.9d: Methoden-Default-UI und Product Language für Product-Flow.
Some checks failed
Test Suite / lint-backend (push) Waiting to run
Test Suite / compose-smoke (push) Waiting to run
Test Suite / k6 /api/health Baseline (push) Blocked by required conditions
Test Suite / playwright-smoke (push) Blocked by required conditions
Deploy Development / deploy (push) Successful in 47s
Test Suite / pytest-backend (push) Has been cancelled
Some checks failed
Test Suite / lint-backend (push) Waiting to run
Test Suite / compose-smoke (push) Waiting to run
Test Suite / k6 /api/health Baseline (push) Blocked by required conditions
Test Suite / playwright-smoke (push) Blocked by required conditions
Deploy Development / deploy (push) Successful in 47s
Test Suite / pytest-backend (push) Has been cancelled
Prozessleiste, archetypgebundene Modus-Defaults und Sprint/Arbeitspaket-Begriffe schließen den GUI-Kern für continuous_product vor Sprint-Planungs-Flows. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
82a5b9adec
commit
09826d146e
|
|
@ -159,4 +159,4 @@ Bei Konflikten:
|
|||
|
||||
---
|
||||
|
||||
*Siehe auch: `Kairo_Implementation_Truth_Table_v0.1.md` § Referenz-Archetypen, `Kairo_Status_Review_and_Next_Steps_v0.2.md`*
|
||||
*Siehe auch: `Kairo_Implementation_Truth_Table_v0.1.md` § Referenz-Archetypen, `Kairo_Status_Review_and_Next_Steps_v0.2.md`, **`Kairo_Session_Handover_2026-07-12.md`** (Session-Feedback & Stop-Regeln)*
|
||||
|
|
|
|||
195
docs/product/Kairo_Session_Handover_2026-07-12.md
Normal file
195
docs/product/Kairo_Session_Handover_2026-07-12.md
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
# Jinkendo Kairo
|
||||
## Session-Handover — 2026-07-12
|
||||
|
||||
**Status:** Übergabe an neuen Chat / nächsten Agenten
|
||||
**Stand:** 2026-07-12 (Abend)
|
||||
**Branch:** `develop` @ `82a5b9a` (gepusht auf Remote)
|
||||
**Version (Deploy):** `0.20.2-ap2.2d`
|
||||
**Vorgänger-Chat:** [MVP-Plan AP2.2 Session](c5b5c664-9bf9-486a-acf9-f20bb005b32e)
|
||||
|
||||
---
|
||||
|
||||
## 1. Wichtigste Klarstellung
|
||||
|
||||
**Es gibt keinen neuen „Plan v0.3“-Bedarf.** Zu Beginn dieser Session wurde der Kurs bereits in **`Kairo_MVP_Execution_Plan_v0.2.md`** festgezogen (PO-Kurskorrektur: Referenz-Archetypen statt Dogfooding-Seed).
|
||||
|
||||
Was in der Session schief lief: **Implementierung lief schneller als das Zielbild** — AP2.2b/c/d wurden als Code-Pakete abgearbeitet, während **End-to-End-Flows und methodengebundene Default-UI (AP1.9d)** fehlen. Der Agent schlug am Ende fälschlich vor, den Plan *noch einmal* neu aufzustellen. **Das lehnt der PO ab.**
|
||||
|
||||
**Für den nächsten Chat gilt:**
|
||||
|
||||
1. **`Kairo_MVP_Execution_Plan_v0.2.md` bleibt führend** — nicht ersetzen, nicht parallel neu erfinden.
|
||||
2. **Dieses Handover** präzisiert Interpretation, PO-Feedback und **Stop-Regeln** für Implementierung.
|
||||
3. Nächste Arbeit = **Zielbild-Flows schließen**, nicht weitere Nav-/Hint-Patches.
|
||||
|
||||
---
|
||||
|
||||
## 2. Session-Verlauf (Kurz)
|
||||
|
||||
| Phase | Inhalt |
|
||||
|-------|--------|
|
||||
| Start | MVP-Plan fortsetzen; Kurskorrektur: mehrere Referenz-Archetypen anleg- und pflegbar (nicht Dogfooding spiegeln) |
|
||||
| AP2.2a | Gepusht (`2b0d5dd`): Starter-Kits, geführte Anlage, `seed_004` deprecated |
|
||||
| AP2.2b/c | Committed + gepusht (`0b57e2e`): A1-Stufenwechsel, Team-Fix, Agent-POST, Steuerungs-Hinweise |
|
||||
| AP2.2d | Committed + gepusht (`82a5b9a`): Sprint-UI, work_cycle-API im Frontend, Backlog→Sprint-Autoassign |
|
||||
| PO-Test | UX-Probleme: zu viele Archetypen, UUIDs im Team, fehlende Actor-Anlage, Reifegrad schwer findbar |
|
||||
| PO-Agile | Zeitbox nicht als Scrum-Kern; Sprint-Planung aus Backlog fehlt; Begriff „Zeitbox“ verwirrend |
|
||||
| Abschluss | Session dokumentieren, sauberer Handover — **kein weiterer Plan-Reset** |
|
||||
|
||||
---
|
||||
|
||||
## 3. Was auf `develop` liegt (Commits)
|
||||
|
||||
```
|
||||
82a5b9a AP2.2d: B2b Product-Backlog und B3 Sprint-Backlog in UI.
|
||||
0b57e2e AP2.2b/c: A1-Stufenwechsel, Team-Fix, Agent-Anlage, Steuerungs-Hinweise.
|
||||
2b0d5dd AP2.2a: Archetyp-Starter-Kits und MVP Execution Plan v0.2.
|
||||
```
|
||||
|
||||
### AP2.2a (Infrastruktur + Anlage)
|
||||
- `backend/services/archetype_starter_kit.py` — Kits A1, A2, B2b, B2a
|
||||
- `GET /api/entity-archetypes/{key}/starter-preview`
|
||||
- `POST /api/initiatives` mit `apply_starter_kit`
|
||||
- Frontend: Vorschau in `InitiativeForm.jsx`
|
||||
|
||||
### AP2.2b/c (Teil-UI, Backend teils fertig)
|
||||
- AP2.0e: `maturity_stage_transition.py` — Recurring rotiert bei Stufen-Verify
|
||||
- Team: `display_name`, `POST /api/actors`, Agent-Formular
|
||||
- `ArchetypeSteeringHints.jsx`, Optgroups „MVP Stufe A“
|
||||
|
||||
### AP2.2d (Teil-UI — **PO: unzureichend für Scrum-Zielbild**)
|
||||
- `frontend/src/api/workCycles.js`, `WorkCyclesPanel.jsx`
|
||||
- Plan → **Sprint**, Ausführen → **Sprint**
|
||||
- Backlog-Convert setzt `work_cycle_id` der aktiven Zeitbox
|
||||
- `agile_iteration`-Strategie bei aktivem Sprint + `continuous_product`
|
||||
- **Fehlt:** Sprint-Planung (Backlog auswählen → in Sprint legen), Sprint-Abschluss, optionaler Sprint ohne Zeit-Druck
|
||||
|
||||
---
|
||||
|
||||
## 4. PO-Feedback (verbindlich für nächste Implementierung)
|
||||
|
||||
### 4.1 Allgemein
|
||||
- **Keine Shortcuts:** Keine Hints/Nav/Redirects als Ersatz für echte methodengebundene Flows.
|
||||
- **Keine Seeds/Vorbefüllung** als Produkt-Erfahrung; Starter-Kits nur optional beim Anlegen, sonst leeres Vorhaben.
|
||||
- **GUI muss Prozess zeigen:** Archetyp → Methode → Plan vs. Ist → Kontrolle — nicht sechs lose Plan-Tabs.
|
||||
- **Abhängigkeiten (Gate-Graph, kritischer Pfad)** bleiben Pflicht (A2, auch unabhängig von Agile).
|
||||
|
||||
### 4.2 Agile / Scrum
|
||||
- **Agile ist Methode** (`agile_iteration`), **kein Archetyp**. Basis: `initiative.product` + `continuous_product`.
|
||||
- **Sprint ist Planungs-Container**, kein Kalender-Zwang. `work_cycle` im Modell, in UI: **„Sprint“** (nicht „Zeitbox“).
|
||||
- Gewünschter Flow:
|
||||
1. Priorisierbares Product Backlog (Eingang)
|
||||
2. Sprint-Planung anstoßen
|
||||
3. Items aus Backlog in Sprint legen (Commit)
|
||||
4. Sprint ausführen
|
||||
5. Sprint abschließen → Review/Carryover → nächste Planung
|
||||
- **Aktueller Ist-Stand:** nur „In Maßnahme umwandeln“ ohne Sprint-Auswahl; Ausführen→Sprint leer ohne aktiven Sprint.
|
||||
|
||||
### 4.3 Verwirrung in der GUI (Ist)
|
||||
- Zu viele gleichwertige Plan-Unterpunkte ohne Prozessleiste
|
||||
- Begriffe: Zeitbox, Maßnahme, Arbeit, Sprint, Eingang — nicht konsistent mit Vision §4 Product Language
|
||||
- AP1.9d (Methoden-Default-UI) **offen** — überall Listen statt Next Action / methodenspezifischer Default
|
||||
|
||||
---
|
||||
|
||||
## 5. Execution Plan v0.2 — Interpretations-Korrektur
|
||||
|
||||
**Plan bleibt gültig.** Folgende Zeilen sind **überzogen als „✓“ markiert** und sollten im nächsten Chat so gelesen werden:
|
||||
|
||||
| Paket | Plan-Status | Tatsächlicher Stand |
|
||||
|-------|-------------|---------------------|
|
||||
| AP2.2a | ✓ | Anlage + Starter-Kit — OK als Basis |
|
||||
| AP2.2b | ◐ | Graph/Strategie da; Kontrolle A2 End-to-End fehlt |
|
||||
| AP2.2c | ◐ | AP2.0e Backend; Journey/Recurring-UX unvollständig |
|
||||
| AP2.2d | ✓ (Plan) | **Nur Infrastruktur + Teil-UI** — Sprint-Planungs-Flow fehlt (ADP § Sprint Planning = ○) |
|
||||
| AP1.9d | offen | **Blocker** für verständliche GUI — vor weiteren AP2.2-Patches priorisieren |
|
||||
|
||||
**Nächste Priorität laut Plan (unverändert):** AP1.9d → AP2.2b/c/d **Flows schließen** → AP1.7b → AP2.1 Validation.
|
||||
|
||||
**Nicht tun:** Execution Plan v0.3 schreiben, es sei denn PO fordert es explizit.
|
||||
|
||||
---
|
||||
|
||||
## 6. Zielbild — ein Bild für alle Archetypen
|
||||
|
||||
Drei Schichten (aus Vision v0.2 + MVP v0.3):
|
||||
|
||||
```text
|
||||
Archetyp (Was?) → Methode (Wie steuern?) → Plan vs. Ist (Was ist wann?)
|
||||
```
|
||||
|
||||
| Archetyp | Methode | Plan | Ist | Kontrolle |
|
||||
|----------|---------|------|-----|-----------|
|
||||
| A2 Linear | `sequential_dependency` | Gate-Graph, Abhängigkeiten | APs an Gates | Nächster ready Schritt am kritischen Pfad |
|
||||
| A1 Reifegrad | `maturity_progression` | Stufen + Graph | Recurring + APs | Aktive Stufe + heutige Übung |
|
||||
| B2b Product | `continuous_product` | Eingang (Product Backlog) | Committete APs | Wirkungsvollster Schritt |
|
||||
| + Agile (Profil) | `agile_iteration` | Sprint-Planung | Sprint-Backlog | Nächster Schritt im Sprint |
|
||||
|
||||
Referenz-UI-Konzept (noch PO-Entwurf): `Kairo_PM_Frontend_UI_Concept_v0.1.md` — Modi + Scope + Objekt; **Prozessleiste pro Methode fehlt noch in Implementierung**.
|
||||
|
||||
---
|
||||
|
||||
## 7. Stop-Regeln für nächsten Agenten
|
||||
|
||||
1. **Kein neuer Execution Plan** ohne PO-Freigabe.
|
||||
2. **Keine weiteren Subnav-Einträge** ohne abgeschlossenen End-to-End-Flow.
|
||||
3. **Keine Seeds / Dogfooding / Demo-Vorhaben** als Implementierungs-Hebel.
|
||||
4. **Kein „✓“** in Truth Table / Execution Plan ohne PO-Durchspielbarkeit (Leitfrage ≤2 Min).
|
||||
5. **Product Language in UI:** Sprint (nicht Zeitbox), Arbeitspaket (nicht Maßnahme wo möglich), Eingang = Product Backlog.
|
||||
6. **Backend-Tests lokal** oft ohne DB — Verifikation über Push auf `develop` → Pi-Deploy (siehe `.cursor/rules/kairo-deployment-testing.mdc`).
|
||||
7. **Nicht proaktiv committen/pushen** ohne Nutzeranfrage.
|
||||
|
||||
---
|
||||
|
||||
## 8. Empfohlener nächster Arbeitsauftrag (für PO-Freigabe)
|
||||
|
||||
**Ein zusammenhängendes Paket** — nicht AP2.2x-Fragmente:
|
||||
|
||||
### Option A (PO-Priorität GUI-Klarheit): AP1.9d + Product Language
|
||||
- Methoden-Default pro Modus (Kontrolle/Ausführen/Plan)
|
||||
- Umbenennung Zeitbox → Sprint in der GUI
|
||||
- Prozessleiste im Scope bei Product-Vorhaben: Eingang → Sprint planen → Ausführen → Kontrolle
|
||||
|
||||
### Option B (Agile-Lücke schließen): Sprint-Planung (Erweiterung AP2.2d, kein neuer Plan)
|
||||
- Backlog-Items markieren → „In Sprint [R2] planen“
|
||||
- Sprint-Status: geplant → aktiv → abgeschlossen
|
||||
- Ausführen ohne aktiven Sprint: Fallback `continuous_product`, nicht leere Sprint-Seite
|
||||
|
||||
### Option C (Abhängigkeiten): AP2.2b A2 End-to-End
|
||||
- Kontrolle: kritischer Pfad + Begründung aus Execution Graph
|
||||
|
||||
**PO sollte eine Option wählen** — nicht alles parallel.
|
||||
|
||||
---
|
||||
|
||||
## 9. Dokumente — Lesereihenfolge nächster Chat
|
||||
|
||||
1. **Dieses Handover**
|
||||
2. `docs/product/Kairo_MVP_Execution_Plan_v0.2.md` (führend, unverändert)
|
||||
3. `docs/product/Kairo_MVP_Definition_v0.3.md`
|
||||
4. `docs/product/Kairo_Vision_and_Product_Direction_v0.2.md` §4–5 (Product Language, Plan vs. Ist)
|
||||
5. `docs/architecture/ADP_Archetype_and_Method_Catalog_v0.2.md` §2.3, § Sprint-Semantik
|
||||
6. `docs/product/Kairo_PM_Frontend_UI_Concept_v0.1.md` (UI-Zielbild)
|
||||
7. `docs/product/Kairo_Implementation_Truth_Table_v0.1.md` (Ist-Check, mit Skepsis gegenüber ✓)
|
||||
|
||||
---
|
||||
|
||||
## 10. Remote-Verifikation (Pi)
|
||||
|
||||
Nach Deploy auf `develop`:
|
||||
- pytest: `test_ap22a_starter_kit`, `test_ap20e_maturity_transition`, `test_ap22_actor_create`, `test_ap22d_sprint_backlog`, `test_ap20f_work_cycle`
|
||||
- Manuell: Product anlegen → Eingang → Sprint anlegen → Convert → Ausführen/Sprint
|
||||
|
||||
---
|
||||
|
||||
## 11. Prompt-Vorschlag für neuen Chat
|
||||
|
||||
```text
|
||||
Lies docs/product/Kairo_Session_Handover_2026-07-12.md und arbeite strikt nach
|
||||
Kairo_MVP_Execution_Plan_v0.2.md. Kein neuer Plan, keine Seeds, keine Shortcuts.
|
||||
PO-Priorität: [Option A / B / C aus Handover §8 eintragen].
|
||||
Zuerst kurz bestätigen, welchen End-to-End-Flow du schließt, dann implementieren.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Erstellt am Abschluss der Session 2026-07-12. Execution Plan v0.2 bleibt maßgeblich; dieses Dokument ist Interpretations- und Feedback-Anhang.*
|
||||
|
|
@ -108,7 +108,7 @@ export function ActionForm({
|
|||
/>
|
||||
{cycleOptions.length > 0 && (
|
||||
<label>
|
||||
Sprint / Zeitbox (optional)
|
||||
Sprint (optional)
|
||||
<select name="work_cycle_id" defaultValue={defaultCycleId}>
|
||||
<option value="">— kein Sprint —</option>
|
||||
{cycleOptions.map((cycle) => (
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const HINTS = {
|
|||
link: { to: '/plan/gates', label: 'Gate-Graph öffnen' },
|
||||
},
|
||||
'initiative.product': {
|
||||
text: 'Product: Eingang triagieren → committen in Sprint-Backlog (aktive Zeitbox).',
|
||||
text: 'Product: Eingang triagieren → in Sprint-Backlog committen.',
|
||||
link: { to: '/plan/sprint', label: 'Sprint planen' },
|
||||
secondaryLink: { to: '/work/sprint', label: 'Sprint ausführen' },
|
||||
},
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ export function BacklogSection({
|
|||
onClick={() => onConvert(item.id)}
|
||||
disabled={busy}
|
||||
>
|
||||
In Maßnahme umwandeln
|
||||
In Arbeitspaket umwandeln
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
|
|
|
|||
53
frontend/src/components/MethodProcessBar.jsx
Normal file
53
frontend/src/components/MethodProcessBar.jsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { NavLink, useLocation } from 'react-router-dom'
|
||||
import { useProgramScope } from '../context/ProgramScopeContext.jsx'
|
||||
import { useMethodUiContext } from '../hooks/useMethodUiContext.js'
|
||||
import { resolveActiveProcessStepKey } from '../config/methodUiDefaults.js'
|
||||
|
||||
export function MethodProcessBar() {
|
||||
const location = useLocation()
|
||||
const { initiativeId, hrefWithScope } = useProgramScope()
|
||||
const { processSteps, archetypeKey, loading } = useMethodUiContext()
|
||||
|
||||
if (!initiativeId || loading || processSteps.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const activeKey = resolveActiveProcessStepKey(location.pathname, processSteps)
|
||||
|
||||
return (
|
||||
<nav
|
||||
className="method-process-bar"
|
||||
aria-label={`Prozess — ${archetypeKey || 'Vorhaben'}`}
|
||||
>
|
||||
<ol className="method-process-bar__steps">
|
||||
{processSteps.map((step, index) => {
|
||||
const isActive = activeKey === step.key
|
||||
return (
|
||||
<li
|
||||
key={step.key}
|
||||
className={
|
||||
'method-process-bar__step' +
|
||||
(isActive ? ' method-process-bar__step--active' : '')
|
||||
}
|
||||
>
|
||||
{index > 0 && (
|
||||
<span className="method-process-bar__sep" aria-hidden="true">
|
||||
→
|
||||
</span>
|
||||
)}
|
||||
<NavLink
|
||||
to={hrefWithScope(step.to)}
|
||||
className={({ isActive: routerActive }) =>
|
||||
'method-process-bar__link' +
|
||||
(routerActive || isActive ? ' method-process-bar__link--active' : '')
|
||||
}
|
||||
>
|
||||
{step.label}
|
||||
</NavLink>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ol>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
|
@ -8,11 +8,13 @@ import { useProgramScope } from '../context/ProgramScopeContext.jsx'
|
|||
import { buildProjectsByParent } from '../utils/projectTree.js'
|
||||
import { projectPath } from '../utils/routes.js'
|
||||
import {
|
||||
PLAN_OUTLINE_NODES,
|
||||
resolvePlanOutlineActiveKey,
|
||||
resolvePlanOutlineProjectId,
|
||||
resolvePlanOutlineActionId,
|
||||
} from '../plan/planOutlineNodes.js'
|
||||
import { resolvePlanOutlineNodes } from '../config/methodUiDefaults.js'
|
||||
import { getInitiative } from '../api/initiatives.js'
|
||||
import { getActiveWorkCycle } from '../api/workCycles.js'
|
||||
import { PlanOutlineWorkTree } from './PlanOutlineWorkTree.jsx'
|
||||
|
||||
function PlanOutlineProjectTree({ projects, parentId, activeProjectId, depth = 0 }) {
|
||||
|
|
@ -60,6 +62,8 @@ export function PlanOutlineNav() {
|
|||
const [projects, setProjects] = useState([])
|
||||
const [openActions, setOpenActions] = useState([])
|
||||
const [executionGraph, setExecutionGraph] = useState(null)
|
||||
const [archetypeKey, setArchetypeKey] = useState(null)
|
||||
const [hasActiveSprint, setHasActiveSprint] = useState(false)
|
||||
const [structureOpen, setStructureOpen] = useState(
|
||||
activeKey === 'structure' || Boolean(activeProjectId),
|
||||
)
|
||||
|
|
@ -75,6 +79,42 @@ export function PlanOutlineNav() {
|
|||
setWorkOpen(activeKey === 'work' || Boolean(activeActionId))
|
||||
}, [activeKey, activeActionId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!initiativeId) {
|
||||
setArchetypeKey(null)
|
||||
setHasActiveSprint(false)
|
||||
return undefined
|
||||
}
|
||||
let cancelled = false
|
||||
Promise.all([
|
||||
getInitiative(initiativeId),
|
||||
getActiveWorkCycle(initiativeId).catch(() => null),
|
||||
])
|
||||
.then(([initiative, activeCycle]) => {
|
||||
if (cancelled) return
|
||||
setArchetypeKey(initiative?.archetype_key || null)
|
||||
setHasActiveSprint(Boolean(activeCycle?.id))
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) {
|
||||
setArchetypeKey(null)
|
||||
setHasActiveSprint(false)
|
||||
}
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [initiativeId])
|
||||
|
||||
const outlineNodes = useMemo(
|
||||
() =>
|
||||
resolvePlanOutlineNodes({
|
||||
archetypeKey,
|
||||
hasActiveSprint,
|
||||
}),
|
||||
[archetypeKey, hasActiveSprint],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!initiativeId) {
|
||||
setProjects([])
|
||||
|
|
@ -149,7 +189,7 @@ export function PlanOutlineNav() {
|
|||
</div>
|
||||
|
||||
<ul className="analysis-split__nav-list">
|
||||
{PLAN_OUTLINE_NODES.map((node) => {
|
||||
{outlineNodes.map((node) => {
|
||||
const needsInitiative = node.requiresInitiative && !initiativeId
|
||||
const isDisabled = node.disabled || needsInitiative
|
||||
const isStructure = node.key === 'structure'
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ export function SteeringSnapshotPanel({
|
|||
|
||||
{active_work_cycle && (
|
||||
<p className="steering-work-cycle-row">
|
||||
<span className="muted">Aktive Zeitbox: </span>
|
||||
<span className="muted">Aktiver Sprint: </span>
|
||||
<strong>{active_work_cycle.title}</strong>
|
||||
{active_work_cycle.goal_description && (
|
||||
<span className="steering-method-description muted">
|
||||
|
|
|
|||
|
|
@ -31,10 +31,10 @@ export function WorkCyclesPanel({
|
|||
<section className="card work-cycles-panel">
|
||||
<div className="section-header">
|
||||
<div>
|
||||
<h2>Sprint / Zeitbox</h2>
|
||||
<h2>Sprint</h2>
|
||||
<p className="section-lead muted">
|
||||
Aktive Zeitbox definiert den Sprint-Backlog — committete Actions mit Zeitbox-Zuweisung
|
||||
erscheinen unter Ausführen → Sprint.
|
||||
Der aktive Sprint definiert den Sprint-Backlog — committete Arbeitspakete mit
|
||||
Sprint-Zuweisung erscheinen unter Ausführen → Sprint.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -50,7 +50,7 @@ export function WorkCyclesPanel({
|
|||
)}
|
||||
</p>
|
||||
) : (
|
||||
<p className="muted">Keine aktive Zeitbox — alle committeten Actions gelten als Product-Ist.</p>
|
||||
<p className="muted">Kein aktiver Sprint — committete Arbeitspakete gelten als Product-Ist.</p>
|
||||
)}
|
||||
|
||||
{canManage && (
|
||||
|
|
@ -82,13 +82,13 @@ export function WorkCyclesPanel({
|
|||
Sofort aktivieren
|
||||
</label>
|
||||
<button type="submit" className="btn btn-primary" disabled={busy}>
|
||||
{busy ? 'Anlegen …' : 'Zeitbox anlegen'}
|
||||
{busy ? 'Anlegen …' : 'Sprint anlegen'}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{workCycles.length === 0 ? (
|
||||
<EmptyState message="Noch keine Zeitboxen angelegt." />
|
||||
<EmptyState message="Noch keine Sprints angelegt." />
|
||||
) : (
|
||||
<ul className="item-list">
|
||||
{workCycles.map((cycle) => (
|
||||
|
|
|
|||
180
frontend/src/config/methodUiDefaults.js
Normal file
180
frontend/src/config/methodUiDefaults.js
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
/**
|
||||
* AP1.9d — Methoden-Default-UI: Archetyp/Methode steuert Prozessleiste,
|
||||
* Modus-Defaults und sichtbare Nav-Knoten (Anti-Todo-Wand).
|
||||
*
|
||||
* @typedef {{ key: string, to: string, label: string, mode: 'plan'|'work'|'control' }} ProcessStep
|
||||
* @typedef {{
|
||||
* archetypeKey: string | null,
|
||||
* methodKey: string | null,
|
||||
* hasActiveSprint: boolean,
|
||||
* }} MethodUiInput
|
||||
* @typedef {{
|
||||
* processSteps: ProcessStep[],
|
||||
* planDefaultRoute: string,
|
||||
* workDefaultRoute: string,
|
||||
* controlDefaultRoute: string,
|
||||
* planOutlineKeys: string[] | null,
|
||||
* workNavKeys: string[] | null,
|
||||
* }} MethodUiDefaults
|
||||
*/
|
||||
|
||||
import { PLAN_OUTLINE_NODES } from '../plan/planOutlineNodes.js'
|
||||
import { WORK_NAV_ITEMS } from './workNav.js'
|
||||
|
||||
const PRODUCT_PROCESS = [
|
||||
{ key: 'inbox', to: '/plan/inbox', label: 'Eingang', mode: 'plan' },
|
||||
{ key: 'sprint-plan', to: '/plan/sprint', label: 'Sprint planen', mode: 'plan' },
|
||||
{ key: 'sprint-work', to: '/work/sprint', label: 'Ausführen', mode: 'work' },
|
||||
{ key: 'control', to: '/control/status', label: 'Kontrolle', mode: 'control' },
|
||||
]
|
||||
|
||||
const LINEAR_PROCESS = [
|
||||
{ key: 'gates', to: '/plan/gates', label: 'Zielzustände', mode: 'plan' },
|
||||
{ key: 'work', to: '/work/today', label: 'Ausführen', mode: 'work' },
|
||||
{ key: 'control', to: '/control/status', label: 'Kontrolle', mode: 'control' },
|
||||
]
|
||||
|
||||
const MATURITY_PROCESS = [
|
||||
{ key: 'gates', to: '/plan/gates', label: 'Stufen', mode: 'plan' },
|
||||
{ key: 'journey', to: '/control/journey', label: 'Rhythmen', mode: 'control' },
|
||||
{ key: 'work', to: '/work/today', label: 'Ausführen', mode: 'work' },
|
||||
{ key: 'control', to: '/control/status', label: 'Kontrolle', mode: 'control' },
|
||||
]
|
||||
|
||||
const PROGRAM_PROCESS = [
|
||||
{ key: 'gates', to: '/plan/gates', label: 'Phasen', mode: 'plan' },
|
||||
{ key: 'inbox', to: '/plan/inbox', label: 'Eingang', mode: 'plan' },
|
||||
{ key: 'work', to: '/work/today', label: 'Ausführen', mode: 'work' },
|
||||
{ key: 'control', to: '/control/status', label: 'Kontrolle', mode: 'control' },
|
||||
]
|
||||
|
||||
const ARCHETYPE_UI = {
|
||||
'initiative.product': {
|
||||
processSteps: PRODUCT_PROCESS,
|
||||
planDefaultRoute: '/plan/inbox',
|
||||
workDefaultRoute: '/work/sprint',
|
||||
controlDefaultRoute: '/control/status',
|
||||
planOutlineKeys: ['profile', 'inbox', 'sprint', 'gates'],
|
||||
workNavKeys: ['sprint', 'today', 'mine'],
|
||||
},
|
||||
'initiative.linear_project': {
|
||||
processSteps: LINEAR_PROCESS,
|
||||
planDefaultRoute: '/plan/gates',
|
||||
workDefaultRoute: '/work/today',
|
||||
controlDefaultRoute: '/control/status',
|
||||
planOutlineKeys: ['profile', 'structure', 'gates', 'inbox', 'work'],
|
||||
workNavKeys: ['today', 'mine', 'sprint'],
|
||||
},
|
||||
'initiative.maturity_journey': {
|
||||
processSteps: MATURITY_PROCESS,
|
||||
planDefaultRoute: '/plan/gates',
|
||||
workDefaultRoute: '/work/today',
|
||||
controlDefaultRoute: '/control/status',
|
||||
planOutlineKeys: ['profile', 'gates', 'inbox', 'work'],
|
||||
workNavKeys: ['today', 'mine', 'sprint'],
|
||||
},
|
||||
'initiative.program': {
|
||||
processSteps: PROGRAM_PROCESS,
|
||||
planDefaultRoute: '/plan/gates',
|
||||
workDefaultRoute: '/work/today',
|
||||
controlDefaultRoute: '/control/status',
|
||||
planOutlineKeys: ['profile', 'structure', 'gates', 'inbox', 'sprint', 'work'],
|
||||
workNavKeys: ['today', 'mine', 'sprint'],
|
||||
},
|
||||
}
|
||||
|
||||
const FALLBACK_DEFAULTS = {
|
||||
processSteps: [],
|
||||
planDefaultRoute: '/plan/profile',
|
||||
workDefaultRoute: '/work/today',
|
||||
controlDefaultRoute: '/control/status',
|
||||
planOutlineKeys: null,
|
||||
workNavKeys: null,
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MethodUiInput} input
|
||||
* @returns {MethodUiDefaults}
|
||||
*/
|
||||
export function resolveMethodUiDefaults(input = {}) {
|
||||
const archetypeKey = input.archetypeKey || null
|
||||
const base = ARCHETYPE_UI[archetypeKey] || FALLBACK_DEFAULTS
|
||||
|
||||
let workDefaultRoute = base.workDefaultRoute
|
||||
if (archetypeKey === 'initiative.product' && !input.hasActiveSprint) {
|
||||
workDefaultRoute = '/work/today'
|
||||
}
|
||||
|
||||
return {
|
||||
processSteps: base.processSteps,
|
||||
planDefaultRoute: base.planDefaultRoute,
|
||||
workDefaultRoute,
|
||||
controlDefaultRoute: base.controlDefaultRoute,
|
||||
planOutlineKeys: base.planOutlineKeys,
|
||||
workNavKeys: base.workNavKeys,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MethodUiInput} input
|
||||
* @returns {ProcessStep[]}
|
||||
*/
|
||||
export function resolveProcessSteps(input = {}) {
|
||||
return resolveMethodUiDefaults(input).processSteps
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MethodUiInput} input
|
||||
* @returns {typeof PLAN_OUTLINE_NODES}
|
||||
*/
|
||||
export function resolvePlanOutlineNodes(input = {}) {
|
||||
const { planOutlineKeys } = resolveMethodUiDefaults(input)
|
||||
if (!planOutlineKeys) return PLAN_OUTLINE_NODES
|
||||
const allowed = new Set(planOutlineKeys)
|
||||
return PLAN_OUTLINE_NODES.filter((node) => allowed.has(node.key))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MethodUiInput} input
|
||||
* @returns {typeof WORK_NAV_ITEMS}
|
||||
*/
|
||||
export function resolveWorkNavItems(input = {}) {
|
||||
const { workNavKeys } = resolveMethodUiDefaults(input)
|
||||
if (!workNavKeys) return WORK_NAV_ITEMS
|
||||
const order = new Map(workNavKeys.map((key, index) => [key, index]))
|
||||
return WORK_NAV_ITEMS.filter((item) => order.has(item.key)).sort(
|
||||
(a, b) => (order.get(a.key) ?? 0) - (order.get(b.key) ?? 0),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktiven Prozessschritt aus Modus + Pfad ableiten.
|
||||
* @param {string} pathname
|
||||
* @param {ProcessStep[]} steps
|
||||
*/
|
||||
export function resolveActiveProcessStepKey(pathname, steps) {
|
||||
const path = (pathname || '').replace(/\/$/, '') || pathname
|
||||
if (!steps.length) return null
|
||||
|
||||
if (path.startsWith('/control/')) {
|
||||
const journey = steps.find((s) => s.key === 'journey')
|
||||
if (journey && path.startsWith('/control/journey')) return 'journey'
|
||||
return steps.find((s) => s.mode === 'control')?.key ?? null
|
||||
}
|
||||
if (path.startsWith('/work/')) {
|
||||
if (path.startsWith('/work/sprint')) {
|
||||
return steps.find((s) => s.key === 'sprint-work')?.key ?? null
|
||||
}
|
||||
return steps.find((s) => s.key === 'work')?.key ?? null
|
||||
}
|
||||
if (path.startsWith('/plan/')) {
|
||||
if (path.startsWith('/plan/inbox')) return 'inbox'
|
||||
if (path.startsWith('/plan/sprint')) return 'sprint-plan'
|
||||
if (path.startsWith('/plan/gates')) return 'gates'
|
||||
}
|
||||
if (path.startsWith('/actions/')) {
|
||||
return steps.find((s) => s.mode === 'work')?.key ?? null
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
68
frontend/src/config/methodUiDefaults.test.js
Normal file
68
frontend/src/config/methodUiDefaults.test.js
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
resolveActiveProcessStepKey,
|
||||
resolveMethodUiDefaults,
|
||||
resolvePlanOutlineNodes,
|
||||
resolveProcessSteps,
|
||||
resolveWorkNavItems,
|
||||
} from './methodUiDefaults.js'
|
||||
|
||||
describe('methodUiDefaults', () => {
|
||||
it('product archetype defaults to Eingang and sprint-centric work nav', () => {
|
||||
const ui = resolveMethodUiDefaults({
|
||||
archetypeKey: 'initiative.product',
|
||||
hasActiveSprint: true,
|
||||
})
|
||||
expect(ui.planDefaultRoute).toBe('/plan/inbox')
|
||||
expect(ui.workDefaultRoute).toBe('/work/sprint')
|
||||
expect(ui.planOutlineKeys).toEqual(['profile', 'inbox', 'sprint', 'gates'])
|
||||
expect(resolveWorkNavItems({ archetypeKey: 'initiative.product' }).map((i) => i.key)).toEqual([
|
||||
'sprint',
|
||||
'today',
|
||||
'mine',
|
||||
])
|
||||
})
|
||||
|
||||
it('product without active sprint falls back work default to today', () => {
|
||||
const ui = resolveMethodUiDefaults({
|
||||
archetypeKey: 'initiative.product',
|
||||
hasActiveSprint: false,
|
||||
})
|
||||
expect(ui.workDefaultRoute).toBe('/work/today')
|
||||
})
|
||||
|
||||
it('exposes product process bar steps', () => {
|
||||
const steps = resolveProcessSteps({ archetypeKey: 'initiative.product' })
|
||||
expect(steps.map((s) => s.label)).toEqual([
|
||||
'Eingang',
|
||||
'Sprint planen',
|
||||
'Ausführen',
|
||||
'Kontrolle',
|
||||
])
|
||||
})
|
||||
|
||||
it('linear archetype defaults to gates and filters plan outline', () => {
|
||||
const ui = resolveMethodUiDefaults({ archetypeKey: 'initiative.linear_project' })
|
||||
expect(ui.planDefaultRoute).toBe('/plan/gates')
|
||||
expect(resolvePlanOutlineNodes({ archetypeKey: 'initiative.linear_project' }).map((n) => n.key)).toEqual([
|
||||
'profile',
|
||||
'structure',
|
||||
'gates',
|
||||
'inbox',
|
||||
'work',
|
||||
])
|
||||
})
|
||||
|
||||
it('generic archetype keeps full plan outline and no process steps', () => {
|
||||
expect(resolveMethodUiDefaults({}).processSteps).toEqual([])
|
||||
expect(resolvePlanOutlineNodes({}).length).toBe(6)
|
||||
})
|
||||
|
||||
it('resolves active process step from pathname', () => {
|
||||
const steps = resolveProcessSteps({ archetypeKey: 'initiative.product' })
|
||||
expect(resolveActiveProcessStepKey('/plan/inbox', steps)).toBe('inbox')
|
||||
expect(resolveActiveProcessStepKey('/plan/sprint', steps)).toBe('sprint-plan')
|
||||
expect(resolveActiveProcessStepKey('/work/sprint', steps)).toBe('sprint-work')
|
||||
expect(resolveActiveProcessStepKey('/control/status', steps)).toBe('control')
|
||||
})
|
||||
})
|
||||
|
|
@ -39,23 +39,23 @@ export const PHASE_LABELS = {
|
|||
|
||||
export const PHASE_DESCRIPTIONS = {
|
||||
capture: 'Ideen und Eingänge erfassen — noch wenig strukturierte Arbeit.',
|
||||
triage: 'Backlog wartet auf Commit — noch keine offene nächste Maßnahme.',
|
||||
structure: 'Vorhaben braucht Struktur — keine offene Maßnahme, Backlog leer.',
|
||||
commit: 'Maßnahmen committen und zuweisen.',
|
||||
execute: 'Aktive Umsetzung — offene Maßnahmen oder Blocker bearbeiten.',
|
||||
triage: 'Backlog wartet auf Commit — noch kein nächster Schritt.',
|
||||
structure: 'Vorhaben braucht Struktur — kein offenes Arbeitspaket, Backlog leer.',
|
||||
commit: 'Arbeitspakete committen und zuweisen.',
|
||||
execute: 'Aktive Umsetzung — offene Arbeitspakete oder Blocker bearbeiten.',
|
||||
verify: 'Nachweise und Ergebnisse belegen.',
|
||||
review: 'Reviews oder Maßnahmen mit Review-Status klären.',
|
||||
review: 'Reviews oder Arbeitspakete mit Review-Status klären.',
|
||||
adapt: 'Anpassen — Backlog, Meilensteine oder Prioritäten prüfen.',
|
||||
closure: 'Vorhaben abgeschlossen oder archiviert.',
|
||||
}
|
||||
|
||||
export const SIGNAL_LABELS = {
|
||||
initiative_terminal: 'Vorhaben abgeschlossen',
|
||||
backlog_awaiting_commit: 'Backlog wartet auf Maßnahme',
|
||||
backlog_awaiting_commit: 'Backlog wartet auf Commit',
|
||||
no_open_work: 'Keine offene Arbeit',
|
||||
actions_blocked: 'Maßnahmen blockiert',
|
||||
actions_blocked: 'Arbeitspakete blockiert',
|
||||
open_blockers: 'Offene Blocker',
|
||||
actions_need_review: 'Maßnahmen brauchen Review',
|
||||
actions_need_review: 'Arbeitspakete brauchen Review',
|
||||
reviews_due: 'Reviews fällig',
|
||||
work_in_progress: 'Arbeit in Umsetzung',
|
||||
}
|
||||
|
|
@ -63,19 +63,19 @@ export const SIGNAL_LABELS = {
|
|||
export const NEXT_ACTION_KIND_LABELS = {
|
||||
action: 'Arbeitspaket',
|
||||
resolve_blocker: 'Blocker klären',
|
||||
assign_action: 'Maßnahme zuweisen',
|
||||
assign_action: 'Arbeitspaket zuweisen',
|
||||
convert_backlog: 'Backlog umwandeln',
|
||||
create_action: 'Maßnahme anlegen',
|
||||
create_action: 'Arbeitspaket anlegen',
|
||||
review_milestone: 'Meilenstein prüfen',
|
||||
recurring_due: 'Rhythmus fällig',
|
||||
}
|
||||
|
||||
export const ATTENTION_KIND_LABELS = {
|
||||
blocked_action: 'Blockierte Maßnahme',
|
||||
blocked_action: 'Blockiertes Arbeitspaket',
|
||||
open_blocker: 'Offener Blocker',
|
||||
high_priority_action: 'High Priority',
|
||||
unassigned_action: 'Ohne Zuweisung',
|
||||
initiative_without_next_action: 'Keine nächste Maßnahme',
|
||||
initiative_without_next_action: 'Kein nächster Schritt',
|
||||
stale_initiative: 'Inaktives Vorhaben',
|
||||
milestone_at_risk: 'Meilenstein gefährdet',
|
||||
gate_graph_blocked: 'Gate blockiert (Graph)',
|
||||
|
|
|
|||
71
frontend/src/hooks/useMethodUiContext.js
Normal file
71
frontend/src/hooks/useMethodUiContext.js
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { getInitiative } from '../api/initiatives.js'
|
||||
import { getActiveWorkCycle } from '../api/workCycles.js'
|
||||
import { getInitiativeSteeringSnapshot } from '../api/initiatives.js'
|
||||
import { useProgramScope } from '../context/ProgramScopeContext.jsx'
|
||||
import { resolveMethodUiDefaults } from '../config/methodUiDefaults.js'
|
||||
|
||||
/**
|
||||
* Lädt Archetyp/Methode für methodengebundene UI (AP1.9d).
|
||||
*/
|
||||
export function useMethodUiContext() {
|
||||
const { initiativeId } = useProgramScope()
|
||||
const [archetypeKey, setArchetypeKey] = useState(null)
|
||||
const [methodKey, setMethodKey] = useState(null)
|
||||
const [hasActiveSprint, setHasActiveSprint] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!initiativeId) {
|
||||
setArchetypeKey(null)
|
||||
setMethodKey(null)
|
||||
setHasActiveSprint(false)
|
||||
return undefined
|
||||
}
|
||||
|
||||
let cancelled = false
|
||||
setLoading(true)
|
||||
|
||||
Promise.all([
|
||||
getInitiative(initiativeId),
|
||||
getActiveWorkCycle(initiativeId).catch(() => null),
|
||||
getInitiativeSteeringSnapshot(initiativeId).catch(() => null),
|
||||
])
|
||||
.then(([initiative, activeCycle, snapshot]) => {
|
||||
if (cancelled) return
|
||||
setArchetypeKey(initiative?.archetype_key || null)
|
||||
setMethodKey(snapshot?.method_key || null)
|
||||
setHasActiveSprint(Boolean(activeCycle?.id))
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) {
|
||||
setArchetypeKey(null)
|
||||
setMethodKey(null)
|
||||
setHasActiveSprint(false)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false)
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [initiativeId])
|
||||
|
||||
const input = useMemo(
|
||||
() => ({ archetypeKey, methodKey, hasActiveSprint }),
|
||||
[archetypeKey, methodKey, hasActiveSprint],
|
||||
)
|
||||
|
||||
const defaults = useMemo(() => resolveMethodUiDefaults(input), [input])
|
||||
|
||||
return {
|
||||
initiativeId,
|
||||
archetypeKey,
|
||||
methodKey,
|
||||
hasActiveSprint,
|
||||
loading,
|
||||
...defaults,
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import { DesktopSidebar } from '../components/DesktopSidebar.jsx'
|
|||
import { BottomNav } from '../components/BottomNav.jsx'
|
||||
import { ContextIndicator } from '../components/ContextIndicator.jsx'
|
||||
import { ProgramScopeBar } from '../components/ProgramScopeBar.jsx'
|
||||
import { MethodProcessBar } from '../components/MethodProcessBar.jsx'
|
||||
import { QuickJump } from '../components/QuickJump.jsx'
|
||||
import { RecentlyOpened } from '../components/RecentlyOpened.jsx'
|
||||
|
||||
|
|
@ -43,10 +44,12 @@ export function AppLayout() {
|
|||
</div>
|
||||
<ContextIndicator variant="mobile" />
|
||||
<ProgramScopeBar />
|
||||
<MethodProcessBar />
|
||||
<RecentlyOpened />
|
||||
</header>
|
||||
<div className="app-chrome app-chrome--desktop">
|
||||
<ProgramScopeBar />
|
||||
<MethodProcessBar />
|
||||
<QuickJump />
|
||||
<RecentlyOpened />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,48 @@
|
|||
import { Navigate } from 'react-router-dom'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useProgramScope } from '../../context/ProgramScopeContext.jsx'
|
||||
import { getInitiative } from '../../api/initiatives.js'
|
||||
import { getActiveWorkCycle } from '../../api/workCycles.js'
|
||||
import { resolveMethodUiDefaults } from '../../config/methodUiDefaults.js'
|
||||
|
||||
/** Portfolio ohne Scope → Übersicht; mit Vorhaben → Profil als Einstieg. */
|
||||
/** Portfolio ohne Scope → Übersicht; mit Vorhaben → methoden-Default (AP1.9d). */
|
||||
export function PlanIndexRedirect() {
|
||||
const { initiativeId, hrefWithScope } = useProgramScope()
|
||||
const [target, setTarget] = useState(null)
|
||||
|
||||
if (initiativeId) {
|
||||
return <Navigate to={hrefWithScope('/plan/profile')} replace />
|
||||
useEffect(() => {
|
||||
if (!initiativeId) {
|
||||
setTarget('/plan/portfolio')
|
||||
return undefined
|
||||
}
|
||||
let cancelled = false
|
||||
Promise.all([
|
||||
getInitiative(initiativeId),
|
||||
getActiveWorkCycle(initiativeId).catch(() => null),
|
||||
])
|
||||
.then(([initiative, activeCycle]) => {
|
||||
if (cancelled) return
|
||||
const defaults = resolveMethodUiDefaults({
|
||||
archetypeKey: initiative?.archetype_key,
|
||||
hasActiveSprint: Boolean(activeCycle?.id),
|
||||
})
|
||||
setTarget(hrefWithScope(defaults.planDefaultRoute))
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setTarget(hrefWithScope('/plan/profile'))
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [initiativeId, hrefWithScope])
|
||||
|
||||
if (!initiativeId) {
|
||||
return <Navigate to="/plan/portfolio" replace />
|
||||
}
|
||||
|
||||
if (!target) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <Navigate to={target} replace />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ function PlanSprintInner() {
|
|||
workCycles={workCycles}
|
||||
activeWorkCycle={activeWorkCycle}
|
||||
sectionTitle="Sprint-Backlog"
|
||||
sectionLead="Arbeitspakete in der aktiven Zeitbox — committet aus dem Product Backlog (Eingang) oder direkt hier."
|
||||
sectionLead="Arbeitspakete im aktiven Sprint — committet aus dem Product Backlog (Eingang) oder direkt hier."
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
@ -102,7 +102,7 @@ function PlanSprintInner() {
|
|||
|
||||
export function PlanSprintPage() {
|
||||
return (
|
||||
<RequireInitiativeScope lead="Sprint-Zeitboxen gehören zu einem Vorhaben im Scope.">
|
||||
<RequireInitiativeScope lead="Sprints gehören zu einem Vorhaben im Scope.">
|
||||
<ScopedInitiativeProvider>
|
||||
<PlanSprintInner />
|
||||
</ScopedInitiativeProvider>
|
||||
|
|
|
|||
|
|
@ -4,23 +4,57 @@ import { ModeAreaShell } from '../../components/ModeAreaShell.jsx'
|
|||
import { ModeAreaNav } from '../../components/ModeAreaNav.jsx'
|
||||
import { ModeShell } from '../../components/ModeShell.jsx'
|
||||
import { WORK_NAV_ITEMS, resolveWorkNavActiveKey } from '../../config/workNav.js'
|
||||
import { resolveWorkNavItems } from '../../config/methodUiDefaults.js'
|
||||
import { useProgramScope } from '../../context/ProgramScopeContext.jsx'
|
||||
import { getActiveWorkCycle } from '../../api/workCycles.js'
|
||||
import { getInitiative } from '../../api/initiatives.js'
|
||||
import { getActiveWorkCycle } from '../../api/workCycles.js'
|
||||
import { resolveMethodUiDefaults } from '../../config/methodUiDefaults.js'
|
||||
|
||||
function WorkAreaNav() {
|
||||
const { initiativeId } = useProgramScope()
|
||||
const [navItems, setNavItems] = useState(WORK_NAV_ITEMS)
|
||||
|
||||
useEffect(() => {
|
||||
if (!initiativeId) {
|
||||
setNavItems(WORK_NAV_ITEMS)
|
||||
return undefined
|
||||
}
|
||||
let cancelled = false
|
||||
Promise.all([
|
||||
getInitiative(initiativeId),
|
||||
getActiveWorkCycle(initiativeId).catch(() => null),
|
||||
])
|
||||
.then(([initiative, activeCycle]) => {
|
||||
if (cancelled) return
|
||||
setNavItems(
|
||||
resolveWorkNavItems({
|
||||
archetypeKey: initiative?.archetype_key,
|
||||
hasActiveSprint: Boolean(activeCycle?.id),
|
||||
}),
|
||||
)
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setNavItems(WORK_NAV_ITEMS)
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [initiativeId])
|
||||
|
||||
return (
|
||||
<ModeAreaNav
|
||||
modeLabel="Ausführen"
|
||||
items={navItems}
|
||||
resolveActiveKey={resolveWorkNavActiveKey}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function WorkLayout() {
|
||||
return (
|
||||
<div className="app-page">
|
||||
<ModeShell modeKey="work" />
|
||||
<ModeAreaShell
|
||||
nav={
|
||||
<ModeAreaNav
|
||||
modeLabel="Ausführen"
|
||||
items={WORK_NAV_ITEMS}
|
||||
resolveActiveKey={resolveWorkNavActiveKey}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ModeAreaShell nav={<WorkAreaNav />}>
|
||||
<Outlet />
|
||||
</ModeAreaShell>
|
||||
</div>
|
||||
|
|
@ -28,7 +62,7 @@ export function WorkLayout() {
|
|||
}
|
||||
|
||||
export function WorkIndexRedirect() {
|
||||
const { initiativeId } = useProgramScope()
|
||||
const { initiativeId, hrefWithScope } = useProgramScope()
|
||||
const [target, setTarget] = useState('/work/today')
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -37,25 +71,25 @@ export function WorkIndexRedirect() {
|
|||
return undefined
|
||||
}
|
||||
let cancelled = false
|
||||
Promise.all([getInitiative(initiativeId), getActiveWorkCycle(initiativeId)])
|
||||
Promise.all([
|
||||
getInitiative(initiativeId),
|
||||
getActiveWorkCycle(initiativeId).catch(() => null),
|
||||
])
|
||||
.then(([initiative, activeCycle]) => {
|
||||
if (cancelled) return
|
||||
if (
|
||||
initiative?.archetype_key === 'initiative.product' &&
|
||||
activeCycle?.id
|
||||
) {
|
||||
setTarget(`/work/sprint?initiative=${initiativeId}`)
|
||||
} else {
|
||||
setTarget(`/work/today?initiative=${initiativeId}`)
|
||||
}
|
||||
const defaults = resolveMethodUiDefaults({
|
||||
archetypeKey: initiative?.archetype_key,
|
||||
hasActiveSprint: Boolean(activeCycle?.id),
|
||||
})
|
||||
setTarget(hrefWithScope(defaults.workDefaultRoute))
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setTarget('/work/today')
|
||||
if (!cancelled) setTarget(hrefWithScope('/work/today'))
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [initiativeId])
|
||||
}, [initiativeId, hrefWithScope])
|
||||
|
||||
return <Navigate to={target} replace />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ function WorkSprintInner() {
|
|||
{!activeWorkCycle && isProductArchetype && (
|
||||
<section className="card">
|
||||
<EmptyState
|
||||
message="Kein aktiver Sprint — lege unter Plan → Sprint eine Zeitbox an und aktiviere sie."
|
||||
message="Kein aktiver Sprint — lege unter Plan → Sprint einen Sprint an und aktiviere ihn."
|
||||
/>
|
||||
<p>
|
||||
<Link to={hrefWithScope('/plan/sprint')} className="link-inline">
|
||||
|
|
|
|||
|
|
@ -607,3 +607,65 @@
|
|||
margin: 0 0 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* AP1.9d — methodengebundene Prozessleiste */
|
||||
.method-process-bar {
|
||||
width: 100%;
|
||||
padding: 0 16px 10px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.method-process-bar {
|
||||
width: auto;
|
||||
flex: 1 1 100%;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.method-process-bar__steps {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
gap: 4px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.method-process-bar__step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.method-process-bar__sep {
|
||||
color: var(--jk-text-subtle);
|
||||
font-size: 12px;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.method-process-bar__link {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
color: var(--jk-text-muted);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.method-process-bar__link:hover {
|
||||
color: var(--jk-primary);
|
||||
background: var(--jk-surface-muted, rgba(0, 0, 0, 0.04));
|
||||
}
|
||||
|
||||
.method-process-bar__link--active,
|
||||
.method-process-bar__step--active .method-process-bar__link {
|
||||
color: var(--jk-primary);
|
||||
font-weight: 600;
|
||||
border-color: var(--jk-primary);
|
||||
background: var(--jk-surface);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
const MODE_DEFAULTS = {
|
||||
cockpit: { portfolioHub: '/cockpit', initiativeHub: '/cockpit' },
|
||||
work: { portfolioHub: '/work/today', initiativeHub: '/work/today' },
|
||||
plan: { portfolioHub: '/plan/portfolio', initiativeHub: '/plan/profile' },
|
||||
plan: { portfolioHub: '/plan/portfolio', initiativeHub: '/plan/inbox' },
|
||||
control: { portfolioHub: '/control/status', initiativeHub: '/control/status' },
|
||||
team: { portfolioHub: '/team', initiativeHub: '/team' },
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user