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 && (
|
{cycleOptions.length > 0 && (
|
||||||
<label>
|
<label>
|
||||||
Sprint / Zeitbox (optional)
|
Sprint (optional)
|
||||||
<select name="work_cycle_id" defaultValue={defaultCycleId}>
|
<select name="work_cycle_id" defaultValue={defaultCycleId}>
|
||||||
<option value="">— kein Sprint —</option>
|
<option value="">— kein Sprint —</option>
|
||||||
{cycleOptions.map((cycle) => (
|
{cycleOptions.map((cycle) => (
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ const HINTS = {
|
||||||
link: { to: '/plan/gates', label: 'Gate-Graph öffnen' },
|
link: { to: '/plan/gates', label: 'Gate-Graph öffnen' },
|
||||||
},
|
},
|
||||||
'initiative.product': {
|
'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' },
|
link: { to: '/plan/sprint', label: 'Sprint planen' },
|
||||||
secondaryLink: { to: '/work/sprint', label: 'Sprint ausführen' },
|
secondaryLink: { to: '/work/sprint', label: 'Sprint ausführen' },
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -188,7 +188,7 @@ export function BacklogSection({
|
||||||
onClick={() => onConvert(item.id)}
|
onClick={() => onConvert(item.id)}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
>
|
>
|
||||||
In Maßnahme umwandeln
|
In Arbeitspaket umwandeln
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<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 { buildProjectsByParent } from '../utils/projectTree.js'
|
||||||
import { projectPath } from '../utils/routes.js'
|
import { projectPath } from '../utils/routes.js'
|
||||||
import {
|
import {
|
||||||
PLAN_OUTLINE_NODES,
|
|
||||||
resolvePlanOutlineActiveKey,
|
resolvePlanOutlineActiveKey,
|
||||||
resolvePlanOutlineProjectId,
|
resolvePlanOutlineProjectId,
|
||||||
resolvePlanOutlineActionId,
|
resolvePlanOutlineActionId,
|
||||||
} from '../plan/planOutlineNodes.js'
|
} 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'
|
import { PlanOutlineWorkTree } from './PlanOutlineWorkTree.jsx'
|
||||||
|
|
||||||
function PlanOutlineProjectTree({ projects, parentId, activeProjectId, depth = 0 }) {
|
function PlanOutlineProjectTree({ projects, parentId, activeProjectId, depth = 0 }) {
|
||||||
|
|
@ -60,6 +62,8 @@ export function PlanOutlineNav() {
|
||||||
const [projects, setProjects] = useState([])
|
const [projects, setProjects] = useState([])
|
||||||
const [openActions, setOpenActions] = useState([])
|
const [openActions, setOpenActions] = useState([])
|
||||||
const [executionGraph, setExecutionGraph] = useState(null)
|
const [executionGraph, setExecutionGraph] = useState(null)
|
||||||
|
const [archetypeKey, setArchetypeKey] = useState(null)
|
||||||
|
const [hasActiveSprint, setHasActiveSprint] = useState(false)
|
||||||
const [structureOpen, setStructureOpen] = useState(
|
const [structureOpen, setStructureOpen] = useState(
|
||||||
activeKey === 'structure' || Boolean(activeProjectId),
|
activeKey === 'structure' || Boolean(activeProjectId),
|
||||||
)
|
)
|
||||||
|
|
@ -75,6 +79,42 @@ export function PlanOutlineNav() {
|
||||||
setWorkOpen(activeKey === 'work' || Boolean(activeActionId))
|
setWorkOpen(activeKey === 'work' || Boolean(activeActionId))
|
||||||
}, [activeKey, 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(() => {
|
useEffect(() => {
|
||||||
if (!initiativeId) {
|
if (!initiativeId) {
|
||||||
setProjects([])
|
setProjects([])
|
||||||
|
|
@ -149,7 +189,7 @@ export function PlanOutlineNav() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul className="analysis-split__nav-list">
|
<ul className="analysis-split__nav-list">
|
||||||
{PLAN_OUTLINE_NODES.map((node) => {
|
{outlineNodes.map((node) => {
|
||||||
const needsInitiative = node.requiresInitiative && !initiativeId
|
const needsInitiative = node.requiresInitiative && !initiativeId
|
||||||
const isDisabled = node.disabled || needsInitiative
|
const isDisabled = node.disabled || needsInitiative
|
||||||
const isStructure = node.key === 'structure'
|
const isStructure = node.key === 'structure'
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,7 @@ export function SteeringSnapshotPanel({
|
||||||
|
|
||||||
{active_work_cycle && (
|
{active_work_cycle && (
|
||||||
<p className="steering-work-cycle-row">
|
<p className="steering-work-cycle-row">
|
||||||
<span className="muted">Aktive Zeitbox: </span>
|
<span className="muted">Aktiver Sprint: </span>
|
||||||
<strong>{active_work_cycle.title}</strong>
|
<strong>{active_work_cycle.title}</strong>
|
||||||
{active_work_cycle.goal_description && (
|
{active_work_cycle.goal_description && (
|
||||||
<span className="steering-method-description muted">
|
<span className="steering-method-description muted">
|
||||||
|
|
|
||||||
|
|
@ -31,10 +31,10 @@ export function WorkCyclesPanel({
|
||||||
<section className="card work-cycles-panel">
|
<section className="card work-cycles-panel">
|
||||||
<div className="section-header">
|
<div className="section-header">
|
||||||
<div>
|
<div>
|
||||||
<h2>Sprint / Zeitbox</h2>
|
<h2>Sprint</h2>
|
||||||
<p className="section-lead muted">
|
<p className="section-lead muted">
|
||||||
Aktive Zeitbox definiert den Sprint-Backlog — committete Actions mit Zeitbox-Zuweisung
|
Der aktive Sprint definiert den Sprint-Backlog — committete Arbeitspakete mit
|
||||||
erscheinen unter Ausführen → Sprint.
|
Sprint-Zuweisung erscheinen unter Ausführen → Sprint.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -50,7 +50,7 @@ export function WorkCyclesPanel({
|
||||||
)}
|
)}
|
||||||
</p>
|
</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 && (
|
{canManage && (
|
||||||
|
|
@ -82,13 +82,13 @@ export function WorkCyclesPanel({
|
||||||
Sofort aktivieren
|
Sofort aktivieren
|
||||||
</label>
|
</label>
|
||||||
<button type="submit" className="btn btn-primary" disabled={busy}>
|
<button type="submit" className="btn btn-primary" disabled={busy}>
|
||||||
{busy ? 'Anlegen …' : 'Zeitbox anlegen'}
|
{busy ? 'Anlegen …' : 'Sprint anlegen'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{workCycles.length === 0 ? (
|
{workCycles.length === 0 ? (
|
||||||
<EmptyState message="Noch keine Zeitboxen angelegt." />
|
<EmptyState message="Noch keine Sprints angelegt." />
|
||||||
) : (
|
) : (
|
||||||
<ul className="item-list">
|
<ul className="item-list">
|
||||||
{workCycles.map((cycle) => (
|
{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 = {
|
export const PHASE_DESCRIPTIONS = {
|
||||||
capture: 'Ideen und Eingänge erfassen — noch wenig strukturierte Arbeit.',
|
capture: 'Ideen und Eingänge erfassen — noch wenig strukturierte Arbeit.',
|
||||||
triage: 'Backlog wartet auf Commit — noch keine offene nächste Maßnahme.',
|
triage: 'Backlog wartet auf Commit — noch kein nächster Schritt.',
|
||||||
structure: 'Vorhaben braucht Struktur — keine offene Maßnahme, Backlog leer.',
|
structure: 'Vorhaben braucht Struktur — kein offenes Arbeitspaket, Backlog leer.',
|
||||||
commit: 'Maßnahmen committen und zuweisen.',
|
commit: 'Arbeitspakete committen und zuweisen.',
|
||||||
execute: 'Aktive Umsetzung — offene Maßnahmen oder Blocker bearbeiten.',
|
execute: 'Aktive Umsetzung — offene Arbeitspakete oder Blocker bearbeiten.',
|
||||||
verify: 'Nachweise und Ergebnisse belegen.',
|
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.',
|
adapt: 'Anpassen — Backlog, Meilensteine oder Prioritäten prüfen.',
|
||||||
closure: 'Vorhaben abgeschlossen oder archiviert.',
|
closure: 'Vorhaben abgeschlossen oder archiviert.',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SIGNAL_LABELS = {
|
export const SIGNAL_LABELS = {
|
||||||
initiative_terminal: 'Vorhaben abgeschlossen',
|
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',
|
no_open_work: 'Keine offene Arbeit',
|
||||||
actions_blocked: 'Maßnahmen blockiert',
|
actions_blocked: 'Arbeitspakete blockiert',
|
||||||
open_blockers: 'Offene Blocker',
|
open_blockers: 'Offene Blocker',
|
||||||
actions_need_review: 'Maßnahmen brauchen Review',
|
actions_need_review: 'Arbeitspakete brauchen Review',
|
||||||
reviews_due: 'Reviews fällig',
|
reviews_due: 'Reviews fällig',
|
||||||
work_in_progress: 'Arbeit in Umsetzung',
|
work_in_progress: 'Arbeit in Umsetzung',
|
||||||
}
|
}
|
||||||
|
|
@ -63,19 +63,19 @@ export const SIGNAL_LABELS = {
|
||||||
export const NEXT_ACTION_KIND_LABELS = {
|
export const NEXT_ACTION_KIND_LABELS = {
|
||||||
action: 'Arbeitspaket',
|
action: 'Arbeitspaket',
|
||||||
resolve_blocker: 'Blocker klären',
|
resolve_blocker: 'Blocker klären',
|
||||||
assign_action: 'Maßnahme zuweisen',
|
assign_action: 'Arbeitspaket zuweisen',
|
||||||
convert_backlog: 'Backlog umwandeln',
|
convert_backlog: 'Backlog umwandeln',
|
||||||
create_action: 'Maßnahme anlegen',
|
create_action: 'Arbeitspaket anlegen',
|
||||||
review_milestone: 'Meilenstein prüfen',
|
review_milestone: 'Meilenstein prüfen',
|
||||||
recurring_due: 'Rhythmus fällig',
|
recurring_due: 'Rhythmus fällig',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ATTENTION_KIND_LABELS = {
|
export const ATTENTION_KIND_LABELS = {
|
||||||
blocked_action: 'Blockierte Maßnahme',
|
blocked_action: 'Blockiertes Arbeitspaket',
|
||||||
open_blocker: 'Offener Blocker',
|
open_blocker: 'Offener Blocker',
|
||||||
high_priority_action: 'High Priority',
|
high_priority_action: 'High Priority',
|
||||||
unassigned_action: 'Ohne Zuweisung',
|
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',
|
stale_initiative: 'Inaktives Vorhaben',
|
||||||
milestone_at_risk: 'Meilenstein gefährdet',
|
milestone_at_risk: 'Meilenstein gefährdet',
|
||||||
gate_graph_blocked: 'Gate blockiert (Graph)',
|
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 { BottomNav } from '../components/BottomNav.jsx'
|
||||||
import { ContextIndicator } from '../components/ContextIndicator.jsx'
|
import { ContextIndicator } from '../components/ContextIndicator.jsx'
|
||||||
import { ProgramScopeBar } from '../components/ProgramScopeBar.jsx'
|
import { ProgramScopeBar } from '../components/ProgramScopeBar.jsx'
|
||||||
|
import { MethodProcessBar } from '../components/MethodProcessBar.jsx'
|
||||||
import { QuickJump } from '../components/QuickJump.jsx'
|
import { QuickJump } from '../components/QuickJump.jsx'
|
||||||
import { RecentlyOpened } from '../components/RecentlyOpened.jsx'
|
import { RecentlyOpened } from '../components/RecentlyOpened.jsx'
|
||||||
|
|
||||||
|
|
@ -43,10 +44,12 @@ export function AppLayout() {
|
||||||
</div>
|
</div>
|
||||||
<ContextIndicator variant="mobile" />
|
<ContextIndicator variant="mobile" />
|
||||||
<ProgramScopeBar />
|
<ProgramScopeBar />
|
||||||
|
<MethodProcessBar />
|
||||||
<RecentlyOpened />
|
<RecentlyOpened />
|
||||||
</header>
|
</header>
|
||||||
<div className="app-chrome app-chrome--desktop">
|
<div className="app-chrome app-chrome--desktop">
|
||||||
<ProgramScopeBar />
|
<ProgramScopeBar />
|
||||||
|
<MethodProcessBar />
|
||||||
<QuickJump />
|
<QuickJump />
|
||||||
<RecentlyOpened />
|
<RecentlyOpened />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,48 @@
|
||||||
import { Navigate } from 'react-router-dom'
|
import { Navigate } from 'react-router-dom'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
import { useProgramScope } from '../../context/ProgramScopeContext.jsx'
|
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() {
|
export function PlanIndexRedirect() {
|
||||||
const { initiativeId, hrefWithScope } = useProgramScope()
|
const { initiativeId, hrefWithScope } = useProgramScope()
|
||||||
|
const [target, setTarget] = useState(null)
|
||||||
|
|
||||||
if (initiativeId) {
|
useEffect(() => {
|
||||||
return <Navigate to={hrefWithScope('/plan/profile')} replace />
|
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 />
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Navigate to="/plan/portfolio" replace />
|
if (!target) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Navigate to={target} replace />
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ function PlanSprintInner() {
|
||||||
workCycles={workCycles}
|
workCycles={workCycles}
|
||||||
activeWorkCycle={activeWorkCycle}
|
activeWorkCycle={activeWorkCycle}
|
||||||
sectionTitle="Sprint-Backlog"
|
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() {
|
export function PlanSprintPage() {
|
||||||
return (
|
return (
|
||||||
<RequireInitiativeScope lead="Sprint-Zeitboxen gehören zu einem Vorhaben im Scope.">
|
<RequireInitiativeScope lead="Sprints gehören zu einem Vorhaben im Scope.">
|
||||||
<ScopedInitiativeProvider>
|
<ScopedInitiativeProvider>
|
||||||
<PlanSprintInner />
|
<PlanSprintInner />
|
||||||
</ScopedInitiativeProvider>
|
</ScopedInitiativeProvider>
|
||||||
|
|
|
||||||
|
|
@ -4,23 +4,57 @@ import { ModeAreaShell } from '../../components/ModeAreaShell.jsx'
|
||||||
import { ModeAreaNav } from '../../components/ModeAreaNav.jsx'
|
import { ModeAreaNav } from '../../components/ModeAreaNav.jsx'
|
||||||
import { ModeShell } from '../../components/ModeShell.jsx'
|
import { ModeShell } from '../../components/ModeShell.jsx'
|
||||||
import { WORK_NAV_ITEMS, resolveWorkNavActiveKey } from '../../config/workNav.js'
|
import { WORK_NAV_ITEMS, resolveWorkNavActiveKey } from '../../config/workNav.js'
|
||||||
|
import { resolveWorkNavItems } from '../../config/methodUiDefaults.js'
|
||||||
import { useProgramScope } from '../../context/ProgramScopeContext.jsx'
|
import { useProgramScope } from '../../context/ProgramScopeContext.jsx'
|
||||||
import { getActiveWorkCycle } from '../../api/workCycles.js'
|
|
||||||
import { getInitiative } from '../../api/initiatives.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() {
|
export function WorkLayout() {
|
||||||
return (
|
return (
|
||||||
<div className="app-page">
|
<div className="app-page">
|
||||||
<ModeShell modeKey="work" />
|
<ModeShell modeKey="work" />
|
||||||
<ModeAreaShell
|
<ModeAreaShell nav={<WorkAreaNav />}>
|
||||||
nav={
|
|
||||||
<ModeAreaNav
|
|
||||||
modeLabel="Ausführen"
|
|
||||||
items={WORK_NAV_ITEMS}
|
|
||||||
resolveActiveKey={resolveWorkNavActiveKey}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</ModeAreaShell>
|
</ModeAreaShell>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -28,7 +62,7 @@ export function WorkLayout() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WorkIndexRedirect() {
|
export function WorkIndexRedirect() {
|
||||||
const { initiativeId } = useProgramScope()
|
const { initiativeId, hrefWithScope } = useProgramScope()
|
||||||
const [target, setTarget] = useState('/work/today')
|
const [target, setTarget] = useState('/work/today')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -37,25 +71,25 @@ export function WorkIndexRedirect() {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
Promise.all([getInitiative(initiativeId), getActiveWorkCycle(initiativeId)])
|
Promise.all([
|
||||||
|
getInitiative(initiativeId),
|
||||||
|
getActiveWorkCycle(initiativeId).catch(() => null),
|
||||||
|
])
|
||||||
.then(([initiative, activeCycle]) => {
|
.then(([initiative, activeCycle]) => {
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
if (
|
const defaults = resolveMethodUiDefaults({
|
||||||
initiative?.archetype_key === 'initiative.product' &&
|
archetypeKey: initiative?.archetype_key,
|
||||||
activeCycle?.id
|
hasActiveSprint: Boolean(activeCycle?.id),
|
||||||
) {
|
})
|
||||||
setTarget(`/work/sprint?initiative=${initiativeId}`)
|
setTarget(hrefWithScope(defaults.workDefaultRoute))
|
||||||
} else {
|
|
||||||
setTarget(`/work/today?initiative=${initiativeId}`)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
if (!cancelled) setTarget('/work/today')
|
if (!cancelled) setTarget(hrefWithScope('/work/today'))
|
||||||
})
|
})
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true
|
cancelled = true
|
||||||
}
|
}
|
||||||
}, [initiativeId])
|
}, [initiativeId, hrefWithScope])
|
||||||
|
|
||||||
return <Navigate to={target} replace />
|
return <Navigate to={target} replace />
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ function WorkSprintInner() {
|
||||||
{!activeWorkCycle && isProductArchetype && (
|
{!activeWorkCycle && isProductArchetype && (
|
||||||
<section className="card">
|
<section className="card">
|
||||||
<EmptyState
|
<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>
|
<p>
|
||||||
<Link to={hrefWithScope('/plan/sprint')} className="link-inline">
|
<Link to={hrefWithScope('/plan/sprint')} className="link-inline">
|
||||||
|
|
|
||||||
|
|
@ -607,3 +607,65 @@
|
||||||
margin: 0 0 12px;
|
margin: 0 0 12px;
|
||||||
font-size: 13px;
|
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 = {
|
const MODE_DEFAULTS = {
|
||||||
cockpit: { portfolioHub: '/cockpit', initiativeHub: '/cockpit' },
|
cockpit: { portfolioHub: '/cockpit', initiativeHub: '/cockpit' },
|
||||||
work: { portfolioHub: '/work/today', initiativeHub: '/work/today' },
|
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' },
|
control: { portfolioHub: '/control/status', initiativeHub: '/control/status' },
|
||||||
team: { portfolioHub: '/team', initiativeHub: '/team' },
|
team: { portfolioHub: '/team', initiativeHub: '/team' },
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user