AP2.3f: Restschuld ? uiFeatures, Route-Registry und slice-bewusste Mutationen.
Some checks failed
Deploy Development / deploy (push) Successful in 46s
Test Suite / pytest-backend (push) Failing after 3m9s
Test Suite / k6 /api/health Baseline (push) Has been skipped
Test Suite / playwright-smoke (push) Has been skipped
Test Suite / lint-backend (push) Successful in 3s
Test Suite / compose-smoke (push) Has been skipped
Some checks failed
Deploy Development / deploy (push) Successful in 46s
Test Suite / pytest-backend (push) Failing after 3m9s
Test Suite / k6 /api/health Baseline (push) Has been skipped
Test Suite / playwright-smoke (push) Has been skipped
Test Suite / lint-backend (push) Successful in 3s
Test Suite / compose-smoke (push) Has been skipped
Archetyp-Profile und Seitenlogik auf Operating Context migriert; methodUiDefaults nur noch Fallback. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
44fda7beb9
commit
c79bfa844f
|
|
@ -1,4 +1,4 @@
|
||||||
"""UI-Profile für Initiative-Archetypen — migriert aus methodUiDefaults.js (AP2.3a)."""
|
"""UI-Profile für Initiative-Archetypen — AP2.3a / AP2.3f."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
@ -37,6 +37,17 @@ _SUPPORT_QUEUE_PROCESS = (
|
||||||
{"key": "control", "to": "/control/status", "label": "Kontrolle", "mode": "control"},
|
{"key": "control", "to": "/control/status", "label": "Kontrolle", "mode": "control"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_CONTENT_PROCESS = (
|
||||||
|
{"key": "gates", "to": "/plan/gates", "label": "Kapitel", "mode": "plan"},
|
||||||
|
{"key": "work", "to": "/work/today", "label": "Ausführen", "mode": "work"},
|
||||||
|
{"key": "control", "to": "/control/status", "label": "Kontrolle", "mode": "control"},
|
||||||
|
)
|
||||||
|
|
||||||
|
_DISPUTE_PROCESS = (
|
||||||
|
{"key": "work", "to": "/work/today", "label": "Ausführen", "mode": "work"},
|
||||||
|
{"key": "control", "to": "/control/status", "label": "Kontrolle", "mode": "control"},
|
||||||
|
)
|
||||||
|
|
||||||
_COMMON_OM_SLICES = (
|
_COMMON_OM_SLICES = (
|
||||||
"actions",
|
"actions",
|
||||||
"blockers",
|
"blockers",
|
||||||
|
|
@ -53,15 +64,25 @@ GENERIC_UI_PROFILE: dict[str, Any] = {
|
||||||
"controlDefaultRoute": "/control/status",
|
"controlDefaultRoute": "/control/status",
|
||||||
"planOutlineKeys": None,
|
"planOutlineKeys": None,
|
||||||
"workNavKeys": None,
|
"workNavKeys": None,
|
||||||
"dataSlices": ["actions", "backlog", "roadmap", "projects", "blockers", *_COMMON_OM_SLICES[1:]],
|
"dataSlices": [
|
||||||
|
"actions",
|
||||||
|
"backlog",
|
||||||
|
"roadmap",
|
||||||
|
"projects",
|
||||||
|
"blockers",
|
||||||
|
*_COMMON_OM_SLICES[1:],
|
||||||
|
],
|
||||||
"enabledModes": ["plan", "work", "control"],
|
"enabledModes": ["plan", "work", "control"],
|
||||||
|
"uiFeatures": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
INITIATIVE_UI_PROFILES: dict[str, dict[str, Any]] = {
|
INITIATIVE_UI_PROFILES: dict[str, dict[str, Any]] = {
|
||||||
|
"initiative.generic": dict(GENERIC_UI_PROFILE),
|
||||||
"initiative.product": {
|
"initiative.product": {
|
||||||
"processSteps": list(_PRODUCT_PROCESS),
|
"processSteps": list(_PRODUCT_PROCESS),
|
||||||
"planDefaultRoute": "/plan/inbox",
|
"planDefaultRoute": "/plan/inbox",
|
||||||
"workDefaultRoute": "/work/sprint",
|
"workDefaultRoute": "/work/sprint",
|
||||||
|
"workDefaultRouteWithoutActiveWorkCycle": "/work/today",
|
||||||
"controlDefaultRoute": "/control/status",
|
"controlDefaultRoute": "/control/status",
|
||||||
"planOutlineKeys": ["profile", "inbox", "sprint", "gates"],
|
"planOutlineKeys": ["profile", "inbox", "sprint", "gates"],
|
||||||
"workNavKeys": ["sprint", "today", "mine"],
|
"workNavKeys": ["sprint", "today", "mine"],
|
||||||
|
|
@ -78,6 +99,10 @@ INITIATIVE_UI_PROFILES: dict[str, dict[str, Any]] = {
|
||||||
"steering_methods",
|
"steering_methods",
|
||||||
],
|
],
|
||||||
"enabledModes": ["plan", "work", "control"],
|
"enabledModes": ["plan", "work", "control"],
|
||||||
|
"uiFeatures": {
|
||||||
|
"continuousProductWorkMode": True,
|
||||||
|
"steeringSnapshotOnWorkSprint": True,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"initiative.linear_project": {
|
"initiative.linear_project": {
|
||||||
"processSteps": list(_LINEAR_PROCESS),
|
"processSteps": list(_LINEAR_PROCESS),
|
||||||
|
|
@ -98,6 +123,9 @@ INITIATIVE_UI_PROFILES: dict[str, dict[str, Any]] = {
|
||||||
"steering_methods",
|
"steering_methods",
|
||||||
],
|
],
|
||||||
"enabledModes": ["plan", "work", "control"],
|
"enabledModes": ["plan", "work", "control"],
|
||||||
|
"uiFeatures": {
|
||||||
|
"criticalPathControl": True,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"initiative.maturity_journey": {
|
"initiative.maturity_journey": {
|
||||||
"processSteps": list(_MATURITY_PROCESS),
|
"processSteps": list(_MATURITY_PROCESS),
|
||||||
|
|
@ -117,6 +145,7 @@ INITIATIVE_UI_PROFILES: dict[str, dict[str, Any]] = {
|
||||||
"steering_methods",
|
"steering_methods",
|
||||||
],
|
],
|
||||||
"enabledModes": ["plan", "work", "control"],
|
"enabledModes": ["plan", "work", "control"],
|
||||||
|
"uiFeatures": {},
|
||||||
},
|
},
|
||||||
"initiative.program": {
|
"initiative.program": {
|
||||||
"processSteps": list(_PROGRAM_PROCESS),
|
"processSteps": list(_PROGRAM_PROCESS),
|
||||||
|
|
@ -138,6 +167,7 @@ INITIATIVE_UI_PROFILES: dict[str, dict[str, Any]] = {
|
||||||
"steering_methods",
|
"steering_methods",
|
||||||
],
|
],
|
||||||
"enabledModes": ["plan", "work", "control"],
|
"enabledModes": ["plan", "work", "control"],
|
||||||
|
"uiFeatures": {},
|
||||||
},
|
},
|
||||||
"initiative.support_queue": {
|
"initiative.support_queue": {
|
||||||
"processSteps": list(_SUPPORT_QUEUE_PROCESS),
|
"processSteps": list(_SUPPORT_QUEUE_PROCESS),
|
||||||
|
|
@ -153,6 +183,7 @@ INITIATIVE_UI_PROFILES: dict[str, dict[str, Any]] = {
|
||||||
"steering_methods",
|
"steering_methods",
|
||||||
],
|
],
|
||||||
"enabledModes": ["plan", "work", "control"],
|
"enabledModes": ["plan", "work", "control"],
|
||||||
|
"uiFeatures": {},
|
||||||
},
|
},
|
||||||
"initiative.recurring_program": {
|
"initiative.recurring_program": {
|
||||||
"processSteps": list(_MATURITY_PROCESS),
|
"processSteps": list(_MATURITY_PROCESS),
|
||||||
|
|
@ -169,6 +200,44 @@ INITIATIVE_UI_PROFILES: dict[str, dict[str, Any]] = {
|
||||||
"steering_methods",
|
"steering_methods",
|
||||||
],
|
],
|
||||||
"enabledModes": ["plan", "work", "control"],
|
"enabledModes": ["plan", "work", "control"],
|
||||||
|
"uiFeatures": {},
|
||||||
|
},
|
||||||
|
"initiative.content_project": {
|
||||||
|
"processSteps": list(_CONTENT_PROCESS),
|
||||||
|
"planDefaultRoute": "/plan/gates",
|
||||||
|
"workDefaultRoute": "/work/today",
|
||||||
|
"controlDefaultRoute": "/control/status",
|
||||||
|
"planOutlineKeys": ["profile", "gates", "inbox", "work"],
|
||||||
|
"workNavKeys": ["today", "mine"],
|
||||||
|
"dataSlices": [
|
||||||
|
"actions",
|
||||||
|
"roadmap",
|
||||||
|
"blockers",
|
||||||
|
"evidence",
|
||||||
|
"decisions",
|
||||||
|
"reviews",
|
||||||
|
"steering_methods",
|
||||||
|
],
|
||||||
|
"enabledModes": ["plan", "work", "control"],
|
||||||
|
"uiFeatures": {},
|
||||||
|
},
|
||||||
|
"initiative.dispute_case": {
|
||||||
|
"processSteps": list(_DISPUTE_PROCESS),
|
||||||
|
"planDefaultRoute": "/plan/profile",
|
||||||
|
"workDefaultRoute": "/work/today",
|
||||||
|
"controlDefaultRoute": "/control/status",
|
||||||
|
"planOutlineKeys": ["profile", "work"],
|
||||||
|
"workNavKeys": ["today", "mine"],
|
||||||
|
"dataSlices": [
|
||||||
|
"actions",
|
||||||
|
"blockers",
|
||||||
|
"evidence",
|
||||||
|
"decisions",
|
||||||
|
"reviews",
|
||||||
|
"steering_methods",
|
||||||
|
],
|
||||||
|
"enabledModes": ["plan", "work", "control"],
|
||||||
|
"uiFeatures": {},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,16 @@ def register() -> None:
|
||||||
label="Verfahren / Konflikt",
|
label="Verfahren / Konflikt",
|
||||||
description="Reaktive Steuerung, Fristen, Entscheidungen",
|
description="Reaktive Steuerung, Fristen, Entscheidungen",
|
||||||
next_action_strategy_key="default",
|
next_action_strategy_key="default",
|
||||||
|
data_slices=frozenset(
|
||||||
|
{
|
||||||
|
"actions",
|
||||||
|
"blockers",
|
||||||
|
"evidence",
|
||||||
|
"decisions",
|
||||||
|
"reviews",
|
||||||
|
"steering_methods",
|
||||||
|
}
|
||||||
|
),
|
||||||
compatible_archetype_keys=frozenset({"initiative.dispute_case"}),
|
compatible_archetype_keys=frozenset({"initiative.dispute_case"}),
|
||||||
)
|
)
|
||||||
register_stub_method(
|
register_stub_method(
|
||||||
|
|
@ -65,5 +75,16 @@ def register() -> None:
|
||||||
label="Kapitel-Entwicklung",
|
label="Kapitel-Entwicklung",
|
||||||
description="Inhaltliche Progression, Reviews",
|
description="Inhaltliche Progression, Reviews",
|
||||||
next_action_strategy_key="default",
|
next_action_strategy_key="default",
|
||||||
|
data_slices=frozenset(
|
||||||
|
{
|
||||||
|
"actions",
|
||||||
|
"roadmap",
|
||||||
|
"blockers",
|
||||||
|
"evidence",
|
||||||
|
"decisions",
|
||||||
|
"reviews",
|
||||||
|
"steering_methods",
|
||||||
|
}
|
||||||
|
),
|
||||||
compatible_archetype_keys=frozenset({"initiative.content_project"}),
|
compatible_archetype_keys=frozenset({"initiative.content_project"}),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,8 @@ Verhindert, dass Zielbild-Dokumente als Ist-Stand gelesen werden.
|
||||||
| Situativer Steuerungskontext (Next Action) | ✗ | 📄 Vision §7.6; AP1.8b deferred |
|
| Situativer Steuerungskontext (Next Action) | ✗ | 📄 Vision §7.6; AP1.8b deferred |
|
||||||
| AttentionItem | ◐ | |
|
| AttentionItem | ◐ | |
|
||||||
| Initiative Steering Snapshot | ✓ | Actions + linked; Archetyp/Guidance AP2.0c |
|
| Initiative Steering Snapshot | ✓ | Actions + linked; Archetyp/Guidance AP2.0c |
|
||||||
|
| Operating Context API | ✓ | AP2.3a: `GET …/operating-context`, `ui_profile_json` Migration 027 |
|
||||||
|
| Archetyp-/Methoden-Plugin-Architektur | ◐ | AP2.3 A–E ✓; AP2.3f Restschuld (Resolver, Route-Gating, uiFeatures, slice reload) ◐ |
|
||||||
| operating_phase | ✗ | entfernt AP1.2 |
|
| operating_phase | ✗ | entfernt AP1.2 |
|
||||||
| signals (Snapshot) | ✓ | `snapshot_signals.py` |
|
| signals (Snapshot) | ✓ | `snapshot_signals.py` |
|
||||||
| Graph Read Models (blocked/ready) | ◐ | AP1.4d/e; Join/OR AP1.15d deferred |
|
| Graph Read Models (blocked/ready) | ◐ | AP1.4d/e; Join/OR AP1.15d deferred |
|
||||||
|
|
@ -117,6 +119,8 @@ Verhindert, dass Zielbild-Dokumente als Ist-Stand gelesen werden.
|
||||||
| Plan-Outline | ◐ | AP1.12a–d: Baum, Modal, Reorder, Actions; AP-Kanten AP1.16c offen |
|
| Plan-Outline | ◐ | AP1.12a–d: Baum, Modal, Reorder, Actions; AP-Kanten AP1.16c offen |
|
||||||
| Execution-Plan (AP-Graph) | ◐ | AP1.16c: Outline + Arbeit-Liste + Action-Detail |
|
| Execution-Plan (AP-Graph) | ◐ | AP1.16c: Outline + Arbeit-Liste + Action-Detail |
|
||||||
| Profil-Modal (Archetyp/EFS) | ◐ | AP1.10c |
|
| Profil-Modal (Archetyp/EFS) | ◐ | AP1.10c |
|
||||||
|
| Operating Profile Resolver (FE) | ◐ | AP2.3b/f: API-first, `methodUiDefaults` Fallback |
|
||||||
|
| Route Gating (Modus-Routen) | ◐ | AP2.3d/f: `OperatingRouteGate`, `MODE_ROUTE_GROUPS` |
|
||||||
| Modal-Bearbeitung (Gates/Profil) | ◐ | viele Sektionen noch Inline-CRUD |
|
| Modal-Bearbeitung (Gates/Profil) | ◐ | viele Sektionen noch Inline-CRUD |
|
||||||
| Admin-UI | ✗ | |
|
| Admin-UI | ✗ | |
|
||||||
| Team-Modus | ✗ | AP1.9f |
|
| Team-Modus | ✗ | AP1.9f |
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
**Stand:** 2026-07-12
|
**Stand:** 2026-07-12
|
||||||
**Ersetzt als Priorisierung:** Dogfooding-Seed als Implementierungs-Hebel; Status-Review §5 (Dogfooding-first)
|
**Ersetzt als Priorisierung:** Dogfooding-Seed als Implementierungs-Hebel; Status-Review §5 (Dogfooding-first)
|
||||||
**Bezug:** `Kairo_MVP_Definition_v0.3.md` §5–9, `ADP_Archetype_and_Method_Catalog_v0.2.md` §3, `Kairo_Corrected_MVP_Roadmap_v0.2.md` (AP-Historie)
|
**Bezug:** `Kairo_MVP_Definition_v0.3.md` §5–9, `ADP_Archetype_and_Method_Catalog_v0.2.md` §3, `Kairo_Corrected_MVP_Roadmap_v0.2.md` (AP-Historie)
|
||||||
|
**Spezifikationsphase (ab 2026-07-24):** `Kairo_Archetype_Specification_Program_v0.1.md` + `docs/product/archetypes/` — Spec vor neuem Archetyp-Code (Welle 2+); ersetzt diesen Plan nicht
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,8 @@ import {
|
||||||
PlanIndexRedirect,
|
PlanIndexRedirect,
|
||||||
ControlLayout,
|
ControlLayout,
|
||||||
ControlIndexRedirect,
|
ControlIndexRedirect,
|
||||||
MODE_ROUTE_COMPONENTS,
|
|
||||||
} from './registry/viewRegistry.js'
|
} from './registry/viewRegistry.js'
|
||||||
import { OperatingRouteGate } from './registry/OperatingRouteGate.jsx'
|
import { renderModeRoutes } from './registry/renderModeRoutes.jsx'
|
||||||
import {
|
import {
|
||||||
WorkspaceRedirect,
|
WorkspaceRedirect,
|
||||||
MyActionsRedirect,
|
MyActionsRedirect,
|
||||||
|
|
@ -65,90 +64,17 @@ function AppRoutes() {
|
||||||
|
|
||||||
<Route path="/work" element={<WorkLayout />}>
|
<Route path="/work" element={<WorkLayout />}>
|
||||||
<Route index element={<WorkIndexRedirect />} />
|
<Route index element={<WorkIndexRedirect />} />
|
||||||
<Route
|
{renderModeRoutes('work')}
|
||||||
path="today"
|
|
||||||
element={
|
|
||||||
<OperatingRouteGate>
|
|
||||||
<MODE_ROUTE_COMPONENTS.workToday />
|
|
||||||
</OperatingRouteGate>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="sprint"
|
|
||||||
element={
|
|
||||||
<OperatingRouteGate>
|
|
||||||
<MODE_ROUTE_COMPONENTS.workSprint />
|
|
||||||
</OperatingRouteGate>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="mine"
|
|
||||||
element={
|
|
||||||
<OperatingRouteGate>
|
|
||||||
<MODE_ROUTE_COMPONENTS.workMine />
|
|
||||||
</OperatingRouteGate>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="/plan" element={<PlanLayout />}>
|
<Route path="/plan" element={<PlanLayout />}>
|
||||||
<Route index element={<PlanIndexRedirect />} />
|
<Route index element={<PlanIndexRedirect />} />
|
||||||
<Route path="portfolio" element={<MODE_ROUTE_COMPONENTS.planPortfolio />} />
|
{renderModeRoutes('plan')}
|
||||||
<Route path="profile" element={<MODE_ROUTE_COMPONENTS.planProfile />} />
|
|
||||||
<Route
|
|
||||||
path="structure"
|
|
||||||
element={
|
|
||||||
<OperatingRouteGate>
|
|
||||||
<MODE_ROUTE_COMPONENTS.planStructure />
|
|
||||||
</OperatingRouteGate>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="gates"
|
|
||||||
element={
|
|
||||||
<OperatingRouteGate>
|
|
||||||
<MODE_ROUTE_COMPONENTS.planGates />
|
|
||||||
</OperatingRouteGate>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="inbox"
|
|
||||||
element={
|
|
||||||
<OperatingRouteGate>
|
|
||||||
<MODE_ROUTE_COMPONENTS.planInbox />
|
|
||||||
</OperatingRouteGate>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="sprint"
|
|
||||||
element={
|
|
||||||
<OperatingRouteGate>
|
|
||||||
<MODE_ROUTE_COMPONENTS.planSprint />
|
|
||||||
</OperatingRouteGate>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route path="work" element={<MODE_ROUTE_COMPONENTS.planWork />} />
|
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="/control" element={<ControlLayout />}>
|
<Route path="/control" element={<ControlLayout />}>
|
||||||
<Route index element={<ControlIndexRedirect />} />
|
<Route index element={<ControlIndexRedirect />} />
|
||||||
<Route path="status" element={<MODE_ROUTE_COMPONENTS.controlStatus />} />
|
{renderModeRoutes('control')}
|
||||||
<Route
|
|
||||||
path="plan-ist"
|
|
||||||
element={
|
|
||||||
<OperatingRouteGate>
|
|
||||||
<MODE_ROUTE_COMPONENTS.controlPlanIst />
|
|
||||||
</OperatingRouteGate>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="journey"
|
|
||||||
element={
|
|
||||||
<OperatingRouteGate>
|
|
||||||
<MODE_ROUTE_COMPONENTS.controlJourney />
|
|
||||||
</OperatingRouteGate>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="/projects/:projectId" element={<ProjectObjectPage />} />
|
<Route path="/projects/:projectId" element={<ProjectObjectPage />} />
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,7 @@ import {
|
||||||
applySliceResults,
|
applySliceResults,
|
||||||
loadOperatingSlices,
|
loadOperatingSlices,
|
||||||
} from '../registry/operatingSliceLoaders.js'
|
} from '../registry/operatingSliceLoaders.js'
|
||||||
|
import { resolveOperatingProfileFromInput } from '../registry/resolveOperatingProfile.js'
|
||||||
|
|
||||||
const InitiativeOperationsContext = createContext(null)
|
const InitiativeOperationsContext = createContext(null)
|
||||||
|
|
||||||
|
|
@ -158,6 +159,22 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
[id, sliceSetters],
|
[id, sliceSetters],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const refreshOm = useCallback(
|
||||||
|
async (...sliceKeys) => {
|
||||||
|
if (!id || !sliceKeys.length) return
|
||||||
|
const unique = [...new Set(sliceKeys.flat())]
|
||||||
|
await reloadSlices(unique)
|
||||||
|
},
|
||||||
|
[id, reloadSlices],
|
||||||
|
)
|
||||||
|
|
||||||
|
const refreshOperatingContext = useCallback(async () => {
|
||||||
|
if (!id) return null
|
||||||
|
const opCtx = await getInitiativeOperatingContext(id).catch(() => null)
|
||||||
|
setOperatingContext(opCtx)
|
||||||
|
return opCtx
|
||||||
|
}, [id])
|
||||||
|
|
||||||
const load = useCallback(async ({ silent = false, sliceKeys = null } = {}) => {
|
const load = useCallback(async ({ silent = false, sliceKeys = null } = {}) => {
|
||||||
if (sessionLoading || !id) return
|
if (sessionLoading || !id) return
|
||||||
if (!silent) setLoading(true)
|
if (!silent) setLoading(true)
|
||||||
|
|
@ -319,7 +336,8 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
setMethodBusy(true)
|
setMethodBusy(true)
|
||||||
try {
|
try {
|
||||||
await updateInitiativeSteeringMethod(id, methodKey)
|
await updateInitiativeSteeringMethod(id, methodKey)
|
||||||
setSteeringSnapshot(await getInitiativeSteeringSnapshot(id))
|
await refreshOperatingContext()
|
||||||
|
await refreshOm(['steering_snapshot', 'steering_methods'])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setSteeringSnapshotError(err.message)
|
setSteeringSnapshotError(err.message)
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -331,7 +349,8 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
setMethodBusy(true)
|
setMethodBusy(true)
|
||||||
try {
|
try {
|
||||||
await updateInitiativeMethodProfile(id, methodProfileKey || null)
|
await updateInitiativeMethodProfile(id, methodProfileKey || null)
|
||||||
setSteeringSnapshot(await getInitiativeSteeringSnapshot(id))
|
await refreshOperatingContext()
|
||||||
|
await refreshOm(['steering_snapshot', 'steering_methods'])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setSteeringSnapshotError(err.message)
|
setSteeringSnapshotError(err.message)
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -354,7 +373,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
assigned_actor_ids: assigned,
|
assigned_actor_ids: assigned,
|
||||||
})
|
})
|
||||||
setShowActionForm(false)
|
setShowActionForm(false)
|
||||||
await load()
|
await refreshOm(['actions'])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -383,7 +402,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
await setActionAssignments(actionId, payload.assigned_actor_ids || [])
|
await setActionAssignments(actionId, payload.assigned_actor_ids || [])
|
||||||
}
|
}
|
||||||
setEditingActionId(null)
|
setEditingActionId(null)
|
||||||
await load()
|
await refreshOm(['actions'])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -410,7 +429,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
if (!capabilities.has('kairo.action.manage')) return
|
if (!capabilities.has('kairo.action.manage')) return
|
||||||
try {
|
try {
|
||||||
await updateAction(action.id, { status })
|
await updateAction(action.id, { status })
|
||||||
await load()
|
await refreshOm(['actions'])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
}
|
}
|
||||||
|
|
@ -420,7 +439,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
setFormBusy(true)
|
setFormBusy(true)
|
||||||
try {
|
try {
|
||||||
await createInitiativeBlocker(id, body)
|
await createInitiativeBlocker(id, body)
|
||||||
await load()
|
await refreshOm(['blockers', 'actions'])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -437,7 +456,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
action_id: actionId,
|
action_id: actionId,
|
||||||
set_action_blocked: true,
|
set_action_blocked: true,
|
||||||
})
|
})
|
||||||
await load()
|
await refreshOm(['blockers', 'actions'])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -448,7 +467,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
async function handleBlockerStatus(blockerId, status) {
|
async function handleBlockerStatus(blockerId, status) {
|
||||||
try {
|
try {
|
||||||
await updateBlocker(blockerId, { status })
|
await updateBlocker(blockerId, { status })
|
||||||
await load()
|
await refreshOm(['blockers', 'actions'])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
}
|
}
|
||||||
|
|
@ -457,7 +476,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
async function handleDeleteBlocker(blockerId) {
|
async function handleDeleteBlocker(blockerId) {
|
||||||
try {
|
try {
|
||||||
await deleteBlocker(blockerId)
|
await deleteBlocker(blockerId)
|
||||||
await load()
|
await refreshOm(['blockers', 'actions'])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
}
|
}
|
||||||
|
|
@ -469,7 +488,11 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
try {
|
try {
|
||||||
await updateInitiative(id, core)
|
await updateInitiative(id, core)
|
||||||
await saveInitiativeDynamicFields(id, dynamicFields)
|
await saveInitiativeDynamicFields(id, dynamicFields)
|
||||||
await load()
|
await refreshOm(['initiative'])
|
||||||
|
if (core.archetype_key) {
|
||||||
|
await refreshOperatingContext()
|
||||||
|
await load({ silent: true })
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
|
|
@ -483,7 +506,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
setFormBusy(true)
|
setFormBusy(true)
|
||||||
try {
|
try {
|
||||||
await createInitiativeBacklogItem(id, body)
|
await createInitiativeBacklogItem(id, body)
|
||||||
await load()
|
await refreshOm(['backlog'])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -495,7 +518,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
setFormBusy(true)
|
setFormBusy(true)
|
||||||
try {
|
try {
|
||||||
await updateBacklogItem(itemId, body)
|
await updateBacklogItem(itemId, body)
|
||||||
await load()
|
await refreshOm(['backlog'])
|
||||||
return true
|
return true
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
|
|
@ -511,7 +534,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
roadmap_item_id: roadmapItemId || undefined,
|
roadmap_item_id: roadmapItemId || undefined,
|
||||||
clear_roadmap_item: !roadmapItemId,
|
clear_roadmap_item: !roadmapItemId,
|
||||||
})
|
})
|
||||||
await load()
|
await refreshOm(['backlog'])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
}
|
}
|
||||||
|
|
@ -520,7 +543,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
async function handleBacklogStatus(itemId, status) {
|
async function handleBacklogStatus(itemId, status) {
|
||||||
try {
|
try {
|
||||||
await updateBacklogItem(itemId, { status })
|
await updateBacklogItem(itemId, { status })
|
||||||
await load()
|
await refreshOm(['backlog'])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
}
|
}
|
||||||
|
|
@ -587,7 +610,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
setFormBusy(true)
|
setFormBusy(true)
|
||||||
try {
|
try {
|
||||||
const created = await createWorkCycle(id, body)
|
const created = await createWorkCycle(id, body)
|
||||||
await load({ silent: true })
|
await refreshOm(['work_cycles', 'actions'])
|
||||||
if (created?.id) setSelectedWorkCycleId(created.id)
|
if (created?.id) setSelectedWorkCycleId(created.id)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
|
|
@ -600,7 +623,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
setFormBusy(true)
|
setFormBusy(true)
|
||||||
try {
|
try {
|
||||||
await activateWorkCycle(id, workCycleId)
|
await activateWorkCycle(id, workCycleId)
|
||||||
await load({ silent: true })
|
await refreshOm(['work_cycles', 'actions'])
|
||||||
setSelectedWorkCycleId(workCycleId)
|
setSelectedWorkCycleId(workCycleId)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
|
|
@ -613,7 +636,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
setFormBusy(true)
|
setFormBusy(true)
|
||||||
try {
|
try {
|
||||||
await completeWorkCycle(id, workCycleId)
|
await completeWorkCycle(id, workCycleId)
|
||||||
await load({ silent: true })
|
await refreshOm(['work_cycles', 'actions'])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -635,7 +658,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await completeWorkCycle(id, workCycleId)
|
await completeWorkCycle(id, workCycleId)
|
||||||
await load({ silent: true })
|
await refreshOm(['work_cycles', 'actions'])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
throw err
|
throw err
|
||||||
|
|
@ -648,7 +671,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
setFormBusy(true)
|
setFormBusy(true)
|
||||||
try {
|
try {
|
||||||
await reactivateWorkCycle(id, workCycleId)
|
await reactivateWorkCycle(id, workCycleId)
|
||||||
await load({ silent: true })
|
await refreshOm(['work_cycles', 'actions'])
|
||||||
setSelectedWorkCycleId(workCycleId)
|
setSelectedWorkCycleId(workCycleId)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
|
|
@ -660,7 +683,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
async function handleDeleteBacklog(itemId) {
|
async function handleDeleteBacklog(itemId) {
|
||||||
try {
|
try {
|
||||||
await deleteBacklogItem(itemId)
|
await deleteBacklogItem(itemId)
|
||||||
await load()
|
await refreshOm(['backlog'])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
}
|
}
|
||||||
|
|
@ -670,7 +693,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
setFormBusy(true)
|
setFormBusy(true)
|
||||||
try {
|
try {
|
||||||
const created = await createInitiativeRoadmapItem(id, body)
|
const created = await createInitiativeRoadmapItem(id, body)
|
||||||
await load()
|
await refreshOm(['roadmap'])
|
||||||
return created
|
return created
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
|
|
@ -683,7 +706,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
async function handleRoadmapItemStatus(itemId, status) {
|
async function handleRoadmapItemStatus(itemId, status) {
|
||||||
try {
|
try {
|
||||||
await updateRoadmapItem(itemId, { status })
|
await updateRoadmapItem(itemId, { status })
|
||||||
await load()
|
await refreshOm(['roadmap'])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
}
|
}
|
||||||
|
|
@ -692,7 +715,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
async function handleUpdateRoadmapItem(itemId, body) {
|
async function handleUpdateRoadmapItem(itemId, body) {
|
||||||
try {
|
try {
|
||||||
await updateRoadmapItem(itemId, body)
|
await updateRoadmapItem(itemId, body)
|
||||||
await load()
|
await refreshOm(['roadmap'])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
}
|
}
|
||||||
|
|
@ -708,7 +731,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
status: 'accepted',
|
status: 'accepted',
|
||||||
})
|
})
|
||||||
await verifyRoadmapItemReached(itemId)
|
await verifyRoadmapItemReached(itemId)
|
||||||
await load()
|
await refreshOm(['roadmap', 'steering_snapshot'])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -720,7 +743,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
setFormBusy(true)
|
setFormBusy(true)
|
||||||
try {
|
try {
|
||||||
await verifyRoadmapItemReached(itemId)
|
await verifyRoadmapItemReached(itemId)
|
||||||
await load()
|
await refreshOm(['roadmap', 'steering_snapshot'])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -731,7 +754,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
async function handleDeleteRoadmapItem(itemId) {
|
async function handleDeleteRoadmapItem(itemId) {
|
||||||
try {
|
try {
|
||||||
await deleteRoadmapItem(itemId)
|
await deleteRoadmapItem(itemId)
|
||||||
await load()
|
await refreshOm(['roadmap'])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
}
|
}
|
||||||
|
|
@ -812,7 +835,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
patches.map((patch) => updateBacklogItem(patch.id, { sort_order: patch.sort_order })),
|
patches.map((patch) => updateBacklogItem(patch.id, { sort_order: patch.sort_order })),
|
||||||
)
|
)
|
||||||
await load()
|
await refreshOm(['backlog'])
|
||||||
return true
|
return true
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
|
|
@ -830,7 +853,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
patches.map((patch) => updateRoadmapItem(patch.id, { sort_order: patch.sort_order })),
|
patches.map((patch) => updateRoadmapItem(patch.id, { sort_order: patch.sort_order })),
|
||||||
)
|
)
|
||||||
await load()
|
await refreshOm(['roadmap'])
|
||||||
return true
|
return true
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
|
|
@ -849,7 +872,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
setFormBusy(true)
|
setFormBusy(true)
|
||||||
try {
|
try {
|
||||||
await createInitiativeEvidence(id, body)
|
await createInitiativeEvidence(id, body)
|
||||||
await load()
|
await refreshOm(['evidence'])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -860,7 +883,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
async function handleEvidenceStatus(evidenceId, status) {
|
async function handleEvidenceStatus(evidenceId, status) {
|
||||||
try {
|
try {
|
||||||
await updateEvidence(evidenceId, { status })
|
await updateEvidence(evidenceId, { status })
|
||||||
await load()
|
await refreshOm(['evidence'])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
}
|
}
|
||||||
|
|
@ -869,7 +892,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
async function handleDeleteEvidence(evidenceId) {
|
async function handleDeleteEvidence(evidenceId) {
|
||||||
try {
|
try {
|
||||||
await deleteEvidence(evidenceId)
|
await deleteEvidence(evidenceId)
|
||||||
await load()
|
await refreshOm(['evidence'])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
}
|
}
|
||||||
|
|
@ -879,7 +902,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
setFormBusy(true)
|
setFormBusy(true)
|
||||||
try {
|
try {
|
||||||
await createInitiativeDecision(id, body)
|
await createInitiativeDecision(id, body)
|
||||||
await load()
|
await refreshOm(['decisions'])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -890,7 +913,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
async function handleDecisionStatus(decisionId, status) {
|
async function handleDecisionStatus(decisionId, status) {
|
||||||
try {
|
try {
|
||||||
await updateDecision(decisionId, { status })
|
await updateDecision(decisionId, { status })
|
||||||
await load()
|
await refreshOm(['decisions'])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
}
|
}
|
||||||
|
|
@ -899,7 +922,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
async function handleDeleteDecision(decisionId) {
|
async function handleDeleteDecision(decisionId) {
|
||||||
try {
|
try {
|
||||||
await deleteDecision(decisionId)
|
await deleteDecision(decisionId)
|
||||||
await load()
|
await refreshOm(['decisions'])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
}
|
}
|
||||||
|
|
@ -909,7 +932,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
setFormBusy(true)
|
setFormBusy(true)
|
||||||
try {
|
try {
|
||||||
await createInitiativeReview(id, body)
|
await createInitiativeReview(id, body)
|
||||||
await load()
|
await refreshOm(['reviews'])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -920,7 +943,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
async function handleReviewStatus(reviewId, status) {
|
async function handleReviewStatus(reviewId, status) {
|
||||||
try {
|
try {
|
||||||
await updateReview(reviewId, { status })
|
await updateReview(reviewId, { status })
|
||||||
await load()
|
await refreshOm(['reviews'])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
}
|
}
|
||||||
|
|
@ -929,7 +952,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
async function handleDeleteReview(reviewId) {
|
async function handleDeleteReview(reviewId) {
|
||||||
try {
|
try {
|
||||||
await deleteReview(reviewId)
|
await deleteReview(reviewId)
|
||||||
await load()
|
await refreshOm(['reviews'])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
}
|
}
|
||||||
|
|
@ -939,7 +962,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
setFormBusy(true)
|
setFormBusy(true)
|
||||||
try {
|
try {
|
||||||
await createInitiativeRecurring(id, body)
|
await createInitiativeRecurring(id, body)
|
||||||
await load()
|
await refreshOm(['recurring'])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -950,7 +973,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
async function handleRecurringStatus(recurringId, status) {
|
async function handleRecurringStatus(recurringId, status) {
|
||||||
try {
|
try {
|
||||||
await updateRecurring(recurringId, { status })
|
await updateRecurring(recurringId, { status })
|
||||||
await load()
|
await refreshOm(['recurring'])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
}
|
}
|
||||||
|
|
@ -959,12 +982,23 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
async function handleDeleteRecurring(recurringId) {
|
async function handleDeleteRecurring(recurringId) {
|
||||||
try {
|
try {
|
||||||
await deleteRecurring(recurringId)
|
await deleteRecurring(recurringId)
|
||||||
await load()
|
await refreshOm(['recurring'])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const operatingProfile = useMemo(
|
||||||
|
() =>
|
||||||
|
resolveOperatingProfileFromInput({
|
||||||
|
operatingContext,
|
||||||
|
hasActiveSprint: Boolean(activeWorkCycle?.id),
|
||||||
|
}),
|
||||||
|
[operatingContext, activeWorkCycle?.id],
|
||||||
|
)
|
||||||
|
|
||||||
|
const uiFeatures = operatingProfile.uiFeatures || {}
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
initiativeId: id,
|
initiativeId: id,
|
||||||
initiative,
|
initiative,
|
||||||
|
|
@ -996,6 +1030,8 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
steeringMethods,
|
steeringMethods,
|
||||||
operatingContext,
|
operatingContext,
|
||||||
dataSlices: operatingContext?.data_slices || [],
|
dataSlices: operatingContext?.data_slices || [],
|
||||||
|
uiFeatures,
|
||||||
|
operatingProfile,
|
||||||
methodBusy,
|
methodBusy,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
|
|
@ -1015,6 +1051,8 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
reloadActors: actorsState.reload,
|
reloadActors: actorsState.reload,
|
||||||
reload: load,
|
reload: load,
|
||||||
reloadSlices,
|
reloadSlices,
|
||||||
|
refreshOm,
|
||||||
|
refreshOperatingContext,
|
||||||
handleMethodChange,
|
handleMethodChange,
|
||||||
handleMethodProfileChange,
|
handleMethodProfileChange,
|
||||||
handleCreateAction,
|
handleCreateAction,
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,6 @@ import { SteeringSnapshotPanel } from '../../components/SteeringSnapshotPanel.js
|
||||||
import { CriticalPathPanel } from '../../components/CriticalPathPanel.jsx'
|
import { CriticalPathPanel } from '../../components/CriticalPathPanel.jsx'
|
||||||
import { NextActionWidget } from '../../widgets/NextActionWidget.jsx'
|
import { NextActionWidget } from '../../widgets/NextActionWidget.jsx'
|
||||||
|
|
||||||
function usesCriticalPathControl(initiative, steeringSnapshot) {
|
|
||||||
const archetypeKey = initiative?.archetype_key
|
|
||||||
const methodKey = steeringSnapshot?.method_key
|
|
||||||
return (
|
|
||||||
archetypeKey === 'initiative.linear_project' || methodKey === 'sequential_dependency'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function InitiativeOverviewPage() {
|
export function InitiativeOverviewPage() {
|
||||||
const {
|
const {
|
||||||
initiativeId,
|
initiativeId,
|
||||||
|
|
@ -26,10 +18,11 @@ export function InitiativeOverviewPage() {
|
||||||
methodBusy,
|
methodBusy,
|
||||||
capabilities,
|
capabilities,
|
||||||
handleMethodChange,
|
handleMethodChange,
|
||||||
|
uiFeatures,
|
||||||
} = useInitiativeOperations()
|
} = useInitiativeOperations()
|
||||||
|
|
||||||
const counts = steeringSnapshot?.counts || {}
|
const counts = steeringSnapshot?.counts || {}
|
||||||
const showCriticalPath = usesCriticalPathControl(initiative, steeringSnapshot)
|
const showCriticalPath = Boolean(uiFeatures.criticalPathControl)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import {
|
||||||
CONTROL_NAV_ITEMS,
|
CONTROL_NAV_ITEMS,
|
||||||
resolveControlNavActiveKey,
|
resolveControlNavActiveKey,
|
||||||
} from '../../config/controlNav.js'
|
} from '../../config/controlNav.js'
|
||||||
|
import { useProgramScope } from '../../context/ProgramScopeContext.jsx'
|
||||||
|
import { useMethodUiContext } from '../../hooks/useMethodUiContext.js'
|
||||||
|
|
||||||
export function ControlLayout() {
|
export function ControlLayout() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -29,5 +31,13 @@ export function ControlLayout() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ControlIndexRedirect() {
|
export function ControlIndexRedirect() {
|
||||||
return <Navigate to="/control/status" replace />
|
const { initiativeId, hrefWithScope } = useProgramScope()
|
||||||
|
const { controlDefaultRoute, loading, operatingContext } = useMethodUiContext()
|
||||||
|
|
||||||
|
if (initiativeId && loading && !operatingContext) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = initiativeId ? hrefWithScope(controlDefaultRoute) : '/control/status'
|
||||||
|
return <Navigate to={target} replace />
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,48 +1,19 @@
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { Navigate } from 'react-router-dom'
|
import { Navigate } from 'react-router-dom'
|
||||||
import { useProgramScope } from '../../context/ProgramScopeContext.jsx'
|
import { useProgramScope } from '../../context/ProgramScopeContext.jsx'
|
||||||
import { getInitiativeOperatingContext } from '../../api/initiatives.js'
|
import { useMethodUiContext } from '../../hooks/useMethodUiContext.js'
|
||||||
import { getActiveWorkCycle } from '../../api/workCycles.js'
|
|
||||||
import { resolveOperatingProfileFromInput } from '../../registry/resolveOperatingProfile.js'
|
|
||||||
|
|
||||||
/** Portfolio ohne Scope → Übersicht; mit Vorhaben → methoden-Default (AP1.9d). */
|
/** Portfolio ohne Scope → Übersicht; mit Vorhaben → methoden-Default aus Operating Context. */
|
||||||
export function PlanIndexRedirect() {
|
export function PlanIndexRedirect() {
|
||||||
const { initiativeId, hrefWithScope } = useProgramScope()
|
const { initiativeId, hrefWithScope } = useProgramScope()
|
||||||
const [target, setTarget] = useState(null)
|
const { planDefaultRoute, loading, operatingContext } = useMethodUiContext()
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!initiativeId) {
|
|
||||||
setTarget('/plan/portfolio')
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
let cancelled = false
|
|
||||||
Promise.all([
|
|
||||||
getInitiativeOperatingContext(initiativeId),
|
|
||||||
getActiveWorkCycle(initiativeId).catch(() => null),
|
|
||||||
])
|
|
||||||
.then(([context, activeCycle]) => {
|
|
||||||
if (cancelled) return
|
|
||||||
const defaults = resolveOperatingProfileFromInput({
|
|
||||||
operatingContext: context,
|
|
||||||
hasActiveSprint: Boolean(activeCycle?.id),
|
|
||||||
})
|
|
||||||
setTarget(hrefWithScope(defaults.planDefaultRoute))
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
if (!cancelled) setTarget(hrefWithScope('/plan/profile'))
|
|
||||||
})
|
|
||||||
return () => {
|
|
||||||
cancelled = true
|
|
||||||
}
|
|
||||||
}, [initiativeId, hrefWithScope])
|
|
||||||
|
|
||||||
if (!initiativeId) {
|
if (!initiativeId) {
|
||||||
return <Navigate to="/plan/portfolio" replace />
|
return <Navigate to="/plan/portfolio" replace />
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!target) {
|
if (loading && !operatingContext) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Navigate to={target} replace />
|
return <Navigate to={hrefWithScope(planDefaultRoute)} replace />
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,22 @@
|
||||||
import { Navigate, Outlet } from 'react-router-dom'
|
import { Navigate, Outlet } from 'react-router-dom'
|
||||||
import { useEffect, useState } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { ModeAreaShell } from '../../components/ModeAreaShell.jsx'
|
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 { getInitiative } from '../../api/initiatives.js'
|
import { useMethodUiContext } from '../../hooks/useMethodUiContext.js'
|
||||||
import { getActiveWorkCycle } from '../../api/workCycles.js'
|
import { resolveWorkNavItemsFromProfile } from '../../registry/resolveOperatingProfile.js'
|
||||||
import { resolveMethodUiDefaults } from '../../config/methodUiDefaults.js'
|
|
||||||
|
|
||||||
function WorkAreaNav() {
|
function WorkAreaNav() {
|
||||||
const { initiativeId } = useProgramScope()
|
const { initiativeId } = useProgramScope()
|
||||||
const [navItems, setNavItems] = useState(WORK_NAV_ITEMS)
|
const { operatingContext, hasActiveSprint, loading } = useMethodUiContext()
|
||||||
|
|
||||||
useEffect(() => {
|
const navItems = useMemo(() => {
|
||||||
if (!initiativeId) {
|
if (!initiativeId) return WORK_NAV_ITEMS
|
||||||
setNavItems(WORK_NAV_ITEMS)
|
if (loading && !operatingContext) return WORK_NAV_ITEMS
|
||||||
return undefined
|
return resolveWorkNavItemsFromProfile({ operatingContext, hasActiveSprint })
|
||||||
}
|
}, [initiativeId, loading, operatingContext, hasActiveSprint])
|
||||||
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 (
|
return (
|
||||||
<ModeAreaNav
|
<ModeAreaNav
|
||||||
|
|
@ -63,33 +40,15 @@ export function WorkLayout() {
|
||||||
|
|
||||||
export function WorkIndexRedirect() {
|
export function WorkIndexRedirect() {
|
||||||
const { initiativeId, hrefWithScope } = useProgramScope()
|
const { initiativeId, hrefWithScope } = useProgramScope()
|
||||||
const [target, setTarget] = useState('/work/today')
|
const { workDefaultRoute, loading, operatingContext } = useMethodUiContext()
|
||||||
|
|
||||||
useEffect(() => {
|
if (!initiativeId) {
|
||||||
if (!initiativeId) {
|
return <Navigate to="/work/today" replace />
|
||||||
setTarget('/work/today')
|
}
|
||||||
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.workDefaultRoute))
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
if (!cancelled) setTarget(hrefWithScope('/work/today'))
|
|
||||||
})
|
|
||||||
return () => {
|
|
||||||
cancelled = true
|
|
||||||
}
|
|
||||||
}, [initiativeId, hrefWithScope])
|
|
||||||
|
|
||||||
return <Navigate to={target} replace />
|
if (loading && !operatingContext) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Navigate to={hrefWithScope(workDefaultRoute)} replace />
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { useEffect, useMemo } from 'react'
|
import { useEffect, useMemo } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { RequireInitiativeScope } from '../../components/RequireInitiativeScope.jsx'
|
import { RequireInitiativeScope } from '../../components/RequireInitiativeScope.jsx'
|
||||||
import { ScopedInitiativeProvider } from '../../layout/ScopedInitiativeProvider.jsx'
|
|
||||||
import { InitiativeActionsHub } from '../../components/InitiativeActionsHub.jsx'
|
import { InitiativeActionsHub } from '../../components/InitiativeActionsHub.jsx'
|
||||||
import { NextActionWidget } from '../../widgets/NextActionWidget.jsx'
|
import { NextActionWidget } from '../../widgets/NextActionWidget.jsx'
|
||||||
import { LoadingState } from '../../components/LoadingState.jsx'
|
import { LoadingState } from '../../components/LoadingState.jsx'
|
||||||
|
|
@ -43,6 +42,7 @@ function WorkSprintInner() {
|
||||||
initiative,
|
initiative,
|
||||||
operatingContext,
|
operatingContext,
|
||||||
dataSlices,
|
dataSlices,
|
||||||
|
uiFeatures,
|
||||||
steeringSnapshot,
|
steeringSnapshot,
|
||||||
steeringSnapshotLoading,
|
steeringSnapshotLoading,
|
||||||
visibleActions,
|
visibleActions,
|
||||||
|
|
@ -50,13 +50,20 @@ function WorkSprintInner() {
|
||||||
} = ops
|
} = ops
|
||||||
|
|
||||||
const supportsWorkCycles = dataSlices.includes('work_cycles')
|
const supportsWorkCycles = dataSlices.includes('work_cycles')
|
||||||
const isContinuousProduct = operatingContext?.method_key === 'continuous_product'
|
const isContinuousProductMode = uiFeatures.continuousProductWorkMode && supportsWorkCycles
|
||||||
|
const loadSteeringSnapshot = uiFeatures.steeringSnapshotOnWorkSprint
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initiativeId && !steeringSnapshot && !steeringSnapshotLoading) {
|
if (initiativeId && loadSteeringSnapshot && !steeringSnapshot && !steeringSnapshotLoading) {
|
||||||
reloadSlices(['steering_snapshot'])
|
reloadSlices(['steering_snapshot'])
|
||||||
}
|
}
|
||||||
}, [initiativeId, steeringSnapshot, steeringSnapshotLoading, reloadSlices])
|
}, [
|
||||||
|
initiativeId,
|
||||||
|
loadSteeringSnapshot,
|
||||||
|
steeringSnapshot,
|
||||||
|
steeringSnapshotLoading,
|
||||||
|
reloadSlices,
|
||||||
|
])
|
||||||
|
|
||||||
const defaultWorkCycleId = activeWorkCycle?.id || ''
|
const defaultWorkCycleId = activeWorkCycle?.id || ''
|
||||||
|
|
||||||
|
|
@ -74,7 +81,7 @@ function WorkSprintInner() {
|
||||||
return <LoadingState message="Lade Sprint-Backlog …" />
|
return <LoadingState message="Lade Sprint-Backlog …" />
|
||||||
}
|
}
|
||||||
|
|
||||||
const isProductArchetype = isContinuousProduct && supportsWorkCycles
|
const isProductArchetype = isContinuousProductMode
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -86,7 +93,7 @@ function WorkSprintInner() {
|
||||||
<h2>Product-Ist (ohne aktiven Sprint)</h2>
|
<h2>Product-Ist (ohne aktiven Sprint)</h2>
|
||||||
<p className="section-lead muted">
|
<p className="section-lead muted">
|
||||||
Kein aktiver Sprint — Kairo steuert nach{' '}
|
Kein aktiver Sprint — Kairo steuert nach{' '}
|
||||||
<strong>continuous_product</strong> (wirkungsvollster Schritt). Sprint-Planung
|
<strong>{operatingContext?.method_key || 'continuous_product'}</strong> (wirkungsvollster Schritt). Sprint-Planung
|
||||||
unter{' '}
|
unter{' '}
|
||||||
<Link to={hrefWithScope('/plan/sprint')} className="link-inline">
|
<Link to={hrefWithScope('/plan/sprint')} className="link-inline">
|
||||||
Plan → Sprint
|
Plan → Sprint
|
||||||
|
|
@ -208,10 +215,8 @@ function WorkSprintInner() {
|
||||||
|
|
||||||
export function WorkSprintPage() {
|
export function WorkSprintPage() {
|
||||||
return (
|
return (
|
||||||
<RequireInitiativeScope lead="Sprint-Backlog ist vorhabenspezifisch — wähle ein Product-Vorhaben im Scope.">
|
<RequireInitiativeScope lead="Sprint-Backlog ist vorhabenspezifisch — wähle ein passendes Vorhaben im Scope.">
|
||||||
<ScopedInitiativeProvider>
|
<WorkSprintInner />
|
||||||
<WorkSprintInner />
|
|
||||||
</ScopedInitiativeProvider>
|
|
||||||
</RequireInitiativeScope>
|
</RequireInitiativeScope>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
66
frontend/src/registry/modeRouteRegistry.js
Normal file
66
frontend/src/registry/modeRouteRegistry.js
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
/**
|
||||||
|
* AP2.3f — Modus-Routen-Matching für Route Gating.
|
||||||
|
*/
|
||||||
|
import { MODE_ROUTE_GROUPS } from './viewRegistry.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} pathname
|
||||||
|
* @returns {{ layout: string, route: import('./viewRegistry.js').ModeRouteDefinition, fullPath: string } | null}
|
||||||
|
*/
|
||||||
|
export function matchModeRoute(pathname) {
|
||||||
|
const normalized = (pathname || '').replace(/\/$/, '') || pathname
|
||||||
|
|
||||||
|
for (const [layout, routes] of Object.entries(MODE_ROUTE_GROUPS)) {
|
||||||
|
for (const route of routes) {
|
||||||
|
const fullPath = route.routePath || `/${layout}/${route.path}`
|
||||||
|
if (normalized === fullPath || normalized.startsWith(`${fullPath}/`)) {
|
||||||
|
return { layout, route, fullPath }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} pathname
|
||||||
|
* @param {{ operatingContext?: object | null, dataSlices?: string[] }} input
|
||||||
|
*/
|
||||||
|
export function isRouteAllowedForMode(pathname, input = {}) {
|
||||||
|
const match = matchModeRoute(pathname)
|
||||||
|
if (!match) return true
|
||||||
|
|
||||||
|
const { route } = match
|
||||||
|
const context = input.operatingContext
|
||||||
|
const dataSlices = input.dataSlices || context?.data_slices || []
|
||||||
|
const archetypeKey = context?.archetype_key || null
|
||||||
|
|
||||||
|
if (route.allowedArchetypes?.length && archetypeKey) {
|
||||||
|
if (!route.allowedArchetypes.includes(archetypeKey)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.requiredSlices?.length) {
|
||||||
|
return route.requiredSlices.every((slice) => dataSlices.includes(slice))
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} pathname
|
||||||
|
* @param {{ operatingContext?: object | null }} input
|
||||||
|
*/
|
||||||
|
export function resolveRedirectForModeRoute(pathname, input = {}) {
|
||||||
|
const match = matchModeRoute(pathname)
|
||||||
|
const layout = match?.layout || 'plan'
|
||||||
|
const profile = input.operatingContext?.ui_profile || {}
|
||||||
|
|
||||||
|
if (layout === 'work') {
|
||||||
|
return profile.workDefaultRoute || '/work/today'
|
||||||
|
}
|
||||||
|
if (layout === 'control') {
|
||||||
|
return profile.controlDefaultRoute || '/control/status'
|
||||||
|
}
|
||||||
|
return profile.planDefaultRoute || '/plan/profile'
|
||||||
|
}
|
||||||
22
frontend/src/registry/renderModeRoutes.jsx
Normal file
22
frontend/src/registry/renderModeRoutes.jsx
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Route } from 'react-router-dom'
|
||||||
|
import { OperatingRouteGate } from './OperatingRouteGate.jsx'
|
||||||
|
import { MODE_ROUTE_COMPONENTS, MODE_ROUTE_GROUPS } from './viewRegistry.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AP2.3f — Modus-Subrouten aus Registry (kein App.jsx-Fork pro Archetyp).
|
||||||
|
* @param {'work'|'plan'|'control'} layout
|
||||||
|
*/
|
||||||
|
export function renderModeRoutes(layout) {
|
||||||
|
const routes = MODE_ROUTE_GROUPS[layout] || []
|
||||||
|
return routes.map((route) => {
|
||||||
|
const Component = MODE_ROUTE_COMPONENTS[route.componentKey]
|
||||||
|
const element = route.gated ? (
|
||||||
|
<OperatingRouteGate>
|
||||||
|
<Component />
|
||||||
|
</OperatingRouteGate>
|
||||||
|
) : (
|
||||||
|
<Component />
|
||||||
|
)
|
||||||
|
return <Route key={`${layout}-${route.path}`} path={route.path} element={element} />
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* AP2.3b — Operating Profile Resolver (API-first, methodUiDefaults als Fallback).
|
* AP2.3b/f — Operating Profile Resolver (API-first, methodUiDefaults nur als Fallback).
|
||||||
*/
|
*/
|
||||||
import { PLAN_OUTLINE_NODES } from '../plan/planOutlineNodes.js'
|
import { PLAN_OUTLINE_NODES } from '../plan/planOutlineNodes.js'
|
||||||
import { WORK_NAV_ITEMS } from '../config/workNav.js'
|
import { WORK_NAV_ITEMS } from '../config/workNav.js'
|
||||||
|
|
@ -7,6 +7,10 @@ import {
|
||||||
resolveMethodUiDefaults,
|
resolveMethodUiDefaults,
|
||||||
resolveActiveProcessStepKey as resolveActiveProcessStepKeyFallback,
|
resolveActiveProcessStepKey as resolveActiveProcessStepKeyFallback,
|
||||||
} from '../config/methodUiDefaults.js'
|
} from '../config/methodUiDefaults.js'
|
||||||
|
import {
|
||||||
|
isRouteAllowedForMode,
|
||||||
|
resolveRedirectForModeRoute,
|
||||||
|
} from './modeRouteRegistry.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('../config/methodUiDefaults.js').ProcessStep} ProcessStep
|
* @typedef {import('../config/methodUiDefaults.js').ProcessStep} ProcessStep
|
||||||
|
|
@ -16,7 +20,7 @@ import {
|
||||||
* archetype_key?: string | null,
|
* archetype_key?: string | null,
|
||||||
* method_key?: string | null,
|
* method_key?: string | null,
|
||||||
* method_profile_key?: string | null,
|
* method_profile_key?: string | null,
|
||||||
* ui_profile?: Partial<MethodUiDefaults> | null,
|
* ui_profile?: Partial<MethodUiDefaults & { uiFeatures?: Record<string, boolean>, workDefaultRouteWithoutActiveWorkCycle?: string }> | null,
|
||||||
* data_slices?: string[],
|
* data_slices?: string[],
|
||||||
* method_capabilities?: object,
|
* method_capabilities?: object,
|
||||||
* }} OperatingContextResponse
|
* }} OperatingContextResponse
|
||||||
|
|
@ -35,32 +39,49 @@ const EMPTY_PROFILE = {
|
||||||
controlDefaultRoute: '/control/status',
|
controlDefaultRoute: '/control/status',
|
||||||
planOutlineKeys: null,
|
planOutlineKeys: null,
|
||||||
workNavKeys: null,
|
workNavKeys: null,
|
||||||
|
uiFeatures: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Methoden-Features ergänzen Archetyp-Profil (bis MethodDefinition ui_features trägt). */
|
||||||
|
const METHOD_UI_FEATURES = {
|
||||||
|
sequential_dependency: { criticalPathControl: true },
|
||||||
|
continuous_product: {
|
||||||
|
continuousProductWorkMode: true,
|
||||||
|
steeringSnapshotOnWorkSprint: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeUiFeatures(profileFeatures, methodKey) {
|
||||||
|
const methodFeatures = METHOD_UI_FEATURES[methodKey] || {}
|
||||||
|
return { ...methodFeatures, ...(profileFeatures || {}) }
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveWorkDefaultRoute(profile, options = {}) {
|
||||||
|
const base = profile.workDefaultRoute || EMPTY_PROFILE.workDefaultRoute
|
||||||
|
if (!options.hasActiveSprint && profile.workDefaultRouteWithoutActiveWorkCycle) {
|
||||||
|
return profile.workDefaultRouteWithoutActiveWorkCycle
|
||||||
|
}
|
||||||
|
return base
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {OperatingContextResponse | null | undefined} context
|
* @param {OperatingContextResponse | null | undefined} context
|
||||||
* @param {{ hasActiveSprint?: boolean }} [options]
|
* @param {{ hasActiveSprint?: boolean }} [options]
|
||||||
* @returns {MethodUiDefaults & { dataSlices: string[] }}
|
* @returns {MethodUiDefaults & { dataSlices: string[], uiFeatures: Record<string, boolean> }}
|
||||||
*/
|
*/
|
||||||
export function resolveOperatingProfile(context, options = {}) {
|
export function resolveOperatingProfile(context, options = {}) {
|
||||||
if (context?.ui_profile) {
|
if (context?.ui_profile) {
|
||||||
const profile = context.ui_profile
|
const profile = context.ui_profile
|
||||||
let workDefaultRoute = profile.workDefaultRoute || EMPTY_PROFILE.workDefaultRoute
|
|
||||||
if (
|
|
||||||
context.archetype_key === 'initiative.product' &&
|
|
||||||
!options.hasActiveSprint
|
|
||||||
) {
|
|
||||||
workDefaultRoute = '/work/today'
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
processSteps: profile.processSteps || [],
|
processSteps: profile.processSteps || [],
|
||||||
planDefaultRoute: profile.planDefaultRoute || EMPTY_PROFILE.planDefaultRoute,
|
planDefaultRoute: profile.planDefaultRoute || EMPTY_PROFILE.planDefaultRoute,
|
||||||
workDefaultRoute,
|
workDefaultRoute: resolveWorkDefaultRoute(profile, options),
|
||||||
controlDefaultRoute:
|
controlDefaultRoute:
|
||||||
profile.controlDefaultRoute || EMPTY_PROFILE.controlDefaultRoute,
|
profile.controlDefaultRoute || EMPTY_PROFILE.controlDefaultRoute,
|
||||||
planOutlineKeys: profile.planOutlineKeys ?? null,
|
planOutlineKeys: profile.planOutlineKeys ?? null,
|
||||||
workNavKeys: profile.workNavKeys ?? null,
|
workNavKeys: profile.workNavKeys ?? null,
|
||||||
dataSlices: context.data_slices || profile.dataSlices || [],
|
dataSlices: context.data_slices || profile.dataSlices || [],
|
||||||
|
uiFeatures: mergeUiFeatures(profile.uiFeatures, context?.method_key),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,12 +93,13 @@ export function resolveOperatingProfile(context, options = {}) {
|
||||||
return {
|
return {
|
||||||
...fallback,
|
...fallback,
|
||||||
dataSlices: context?.data_slices || [],
|
dataSlices: context?.data_slices || [],
|
||||||
|
uiFeatures: mergeUiFeatures({}, context?.method_key),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {ResolveOperatingProfileInput} input
|
* @param {ResolveOperatingProfileInput} input
|
||||||
* @returns {MethodUiDefaults & { dataSlices: string[] }}
|
* @returns {MethodUiDefaults & { dataSlices: string[], uiFeatures: Record<string, boolean> }}
|
||||||
*/
|
*/
|
||||||
export function resolveOperatingProfileFromInput(input = {}) {
|
export function resolveOperatingProfileFromInput(input = {}) {
|
||||||
if (input.operatingContext) {
|
if (input.operatingContext) {
|
||||||
|
|
@ -90,7 +112,7 @@ export function resolveOperatingProfileFromInput(input = {}) {
|
||||||
methodKey: input.methodKey || null,
|
methodKey: input.methodKey || null,
|
||||||
hasActiveSprint: input.hasActiveSprint,
|
hasActiveSprint: input.hasActiveSprint,
|
||||||
})
|
})
|
||||||
return { ...fallback, dataSlices: [] }
|
return { ...fallback, dataSlices: [], uiFeatures: {} }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -132,23 +154,10 @@ export function resolveActiveProcessStepKey(pathname, steps) {
|
||||||
*/
|
*/
|
||||||
export function isRouteAllowedForProfile(routePath, input = {}) {
|
export function isRouteAllowedForProfile(routePath, input = {}) {
|
||||||
const profile = resolveOperatingProfileFromInput(input)
|
const profile = resolveOperatingProfileFromInput(input)
|
||||||
const normalized = (routePath || '').replace(/\/$/, '')
|
return isRouteAllowedForMode(routePath, {
|
||||||
|
operatingContext: input.operatingContext,
|
||||||
if (normalized.startsWith('/plan/sprint') || normalized.startsWith('/work/sprint')) {
|
dataSlices: profile.dataSlices,
|
||||||
return profile.dataSlices.includes('work_cycles')
|
})
|
||||||
}
|
|
||||||
if (normalized.startsWith('/plan/structure')) {
|
|
||||||
return profile.dataSlices.includes('projects')
|
|
||||||
}
|
|
||||||
if (normalized.startsWith('/plan/inbox')) {
|
|
||||||
return profile.dataSlices.includes('backlog')
|
|
||||||
}
|
|
||||||
if (normalized.startsWith('/control/journey')) {
|
|
||||||
const steps = profile.processSteps || []
|
|
||||||
return steps.some((s) => s.key === 'journey')
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -157,14 +166,12 @@ export function isRouteAllowedForProfile(routePath, input = {}) {
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
export function resolveRedirectForDisallowedRoute(routePath, input = {}) {
|
export function resolveRedirectForDisallowedRoute(routePath, input = {}) {
|
||||||
|
if (input.operatingContext) {
|
||||||
|
return resolveRedirectForModeRoute(routePath, input)
|
||||||
|
}
|
||||||
const profile = resolveOperatingProfileFromInput(input)
|
const profile = resolveOperatingProfileFromInput(input)
|
||||||
const normalized = (routePath || '').replace(/\/$/, '')
|
const normalized = (routePath || '').replace(/\/$/, '')
|
||||||
|
if (normalized.startsWith('/work/')) return profile.workDefaultRoute
|
||||||
if (normalized.startsWith('/work/')) {
|
if (normalized.startsWith('/control/')) return profile.controlDefaultRoute
|
||||||
return profile.workDefaultRoute
|
|
||||||
}
|
|
||||||
if (normalized.startsWith('/control/')) {
|
|
||||||
return profile.controlDefaultRoute
|
|
||||||
}
|
|
||||||
return profile.planDefaultRoute
|
return profile.planDefaultRoute
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ const PRODUCT_CONTEXT = {
|
||||||
],
|
],
|
||||||
planDefaultRoute: '/plan/inbox',
|
planDefaultRoute: '/plan/inbox',
|
||||||
workDefaultRoute: '/work/sprint',
|
workDefaultRoute: '/work/sprint',
|
||||||
|
workDefaultRouteWithoutActiveWorkCycle: '/work/today',
|
||||||
controlDefaultRoute: '/control/status',
|
controlDefaultRoute: '/control/status',
|
||||||
planOutlineKeys: ['profile', 'inbox', 'sprint', 'gates'],
|
planOutlineKeys: ['profile', 'inbox', 'sprint', 'gates'],
|
||||||
workNavKeys: ['sprint', 'today', 'mine'],
|
workNavKeys: ['sprint', 'today', 'mine'],
|
||||||
|
|
@ -73,4 +74,25 @@ describe('resolveOperatingProfile', () => {
|
||||||
const input = { operatingContext: PRODUCT_CONTEXT }
|
const input = { operatingContext: PRODUCT_CONTEXT }
|
||||||
expect(isRouteAllowedForProfile('/plan/sprint', input)).toBe(true)
|
expect(isRouteAllowedForProfile('/plan/sprint', input)).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('liefert uiFeatures aus Archetyp-Profil', () => {
|
||||||
|
const profile = resolveOperatingProfile({
|
||||||
|
...PRODUCT_CONTEXT,
|
||||||
|
ui_profile: {
|
||||||
|
...PRODUCT_CONTEXT.ui_profile,
|
||||||
|
uiFeatures: { continuousProductWorkMode: true },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(profile.uiFeatures.continuousProductWorkMode).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sequential_dependency ergänzt criticalPathControl', () => {
|
||||||
|
const profile = resolveOperatingProfile({
|
||||||
|
archetype_key: 'initiative.generic',
|
||||||
|
method_key: 'sequential_dependency',
|
||||||
|
ui_profile: { uiFeatures: {} },
|
||||||
|
data_slices: ['actions'],
|
||||||
|
})
|
||||||
|
expect(profile.uiFeatures.criticalPathControl).toBe(true)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -144,36 +144,54 @@ export const MODE_ROUTE_COMPONENTS = {
|
||||||
controlJourney: ControlJourneyPage,
|
controlJourney: ControlJourneyPage,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Modus-Routen mit Slice-Gating (AP2.3d). */
|
/** Modus-Routen mit Slice-Gating (AP2.3d/f). */
|
||||||
|
/** @typedef {{ path: string, componentKey: keyof typeof MODE_ROUTE_COMPONENTS, requiredSlices?: string[], allowedArchetypes?: string[], routePath?: string, gated?: boolean }} ModeRouteDefinition */
|
||||||
|
|
||||||
|
export const MODE_ROUTE_GROUPS = {
|
||||||
|
work: [
|
||||||
|
{ path: 'today', componentKey: 'workToday', requiredSlices: ['actions'], gated: true },
|
||||||
|
{ path: 'mine', componentKey: 'workMine', requiredSlices: ['actions'], gated: true },
|
||||||
|
{
|
||||||
|
path: 'sprint',
|
||||||
|
componentKey: 'workSprint',
|
||||||
|
requiredSlices: ['work_cycles'],
|
||||||
|
routePath: '/work/sprint',
|
||||||
|
gated: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
plan: [
|
||||||
|
{ path: 'portfolio', componentKey: 'planPortfolio', gated: false },
|
||||||
|
{ path: 'profile', componentKey: 'planProfile', gated: false },
|
||||||
|
{ path: 'structure', componentKey: 'planStructure', requiredSlices: ['projects'], gated: true },
|
||||||
|
{ path: 'gates', componentKey: 'planGates', requiredSlices: ['roadmap'], gated: true },
|
||||||
|
{ path: 'inbox', componentKey: 'planInbox', requiredSlices: ['backlog'], gated: true },
|
||||||
|
{
|
||||||
|
path: 'sprint',
|
||||||
|
componentKey: 'planSprint',
|
||||||
|
requiredSlices: ['work_cycles'],
|
||||||
|
routePath: '/plan/sprint',
|
||||||
|
gated: true,
|
||||||
|
},
|
||||||
|
{ path: 'work', componentKey: 'planWork', requiredSlices: ['actions'], gated: false },
|
||||||
|
],
|
||||||
|
control: [
|
||||||
|
{ path: 'status', componentKey: 'controlStatus', gated: false },
|
||||||
|
{ path: 'plan-ist', componentKey: 'controlPlanIst', requiredSlices: ['roadmap'], gated: true },
|
||||||
|
{
|
||||||
|
path: 'journey',
|
||||||
|
componentKey: 'controlJourney',
|
||||||
|
requiredSlices: ['roadmap'],
|
||||||
|
routePath: '/control/journey',
|
||||||
|
gated: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use MODE_ROUTE_GROUPS */
|
||||||
export const MODE_ROUTES = [
|
export const MODE_ROUTES = [
|
||||||
{ path: 'today', componentKey: 'workToday', requiredSlices: ['actions'] },
|
...MODE_ROUTE_GROUPS.work,
|
||||||
{ path: 'mine', componentKey: 'workMine', requiredSlices: ['actions'] },
|
...MODE_ROUTE_GROUPS.plan,
|
||||||
{
|
...MODE_ROUTE_GROUPS.control,
|
||||||
path: 'sprint',
|
|
||||||
componentKey: 'workSprint',
|
|
||||||
requiredSlices: ['work_cycles'],
|
|
||||||
routePath: '/work/sprint',
|
|
||||||
},
|
|
||||||
{ path: 'portfolio', componentKey: 'planPortfolio', requiredSlices: [] },
|
|
||||||
{ path: 'profile', componentKey: 'planProfile', requiredSlices: [] },
|
|
||||||
{ path: 'structure', componentKey: 'planStructure', requiredSlices: ['projects'] },
|
|
||||||
{ path: 'gates', componentKey: 'planGates', requiredSlices: ['roadmap'] },
|
|
||||||
{ path: 'inbox', componentKey: 'planInbox', requiredSlices: ['backlog'] },
|
|
||||||
{
|
|
||||||
path: 'sprint',
|
|
||||||
componentKey: 'planSprint',
|
|
||||||
requiredSlices: ['work_cycles'],
|
|
||||||
routePath: '/plan/sprint',
|
|
||||||
},
|
|
||||||
{ path: 'work', componentKey: 'planWork', requiredSlices: ['actions'] },
|
|
||||||
{ path: 'status', componentKey: 'controlStatus', requiredSlices: [] },
|
|
||||||
{ path: 'plan-ist', componentKey: 'controlPlanIst', requiredSlices: ['roadmap'] },
|
|
||||||
{
|
|
||||||
path: 'journey',
|
|
||||||
componentKey: 'controlJourney',
|
|
||||||
requiredSlices: ['roadmap'],
|
|
||||||
routePath: '/control/journey',
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user