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
|
||||
|
||||
|
|
@ -37,6 +37,17 @@ _SUPPORT_QUEUE_PROCESS = (
|
|||
{"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 = (
|
||||
"actions",
|
||||
"blockers",
|
||||
|
|
@ -53,15 +64,25 @@ GENERIC_UI_PROFILE: dict[str, Any] = {
|
|||
"controlDefaultRoute": "/control/status",
|
||||
"planOutlineKeys": 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"],
|
||||
"uiFeatures": {},
|
||||
}
|
||||
|
||||
INITIATIVE_UI_PROFILES: dict[str, dict[str, Any]] = {
|
||||
"initiative.generic": dict(GENERIC_UI_PROFILE),
|
||||
"initiative.product": {
|
||||
"processSteps": list(_PRODUCT_PROCESS),
|
||||
"planDefaultRoute": "/plan/inbox",
|
||||
"workDefaultRoute": "/work/sprint",
|
||||
"workDefaultRouteWithoutActiveWorkCycle": "/work/today",
|
||||
"controlDefaultRoute": "/control/status",
|
||||
"planOutlineKeys": ["profile", "inbox", "sprint", "gates"],
|
||||
"workNavKeys": ["sprint", "today", "mine"],
|
||||
|
|
@ -78,6 +99,10 @@ INITIATIVE_UI_PROFILES: dict[str, dict[str, Any]] = {
|
|||
"steering_methods",
|
||||
],
|
||||
"enabledModes": ["plan", "work", "control"],
|
||||
"uiFeatures": {
|
||||
"continuousProductWorkMode": True,
|
||||
"steeringSnapshotOnWorkSprint": True,
|
||||
},
|
||||
},
|
||||
"initiative.linear_project": {
|
||||
"processSteps": list(_LINEAR_PROCESS),
|
||||
|
|
@ -98,6 +123,9 @@ INITIATIVE_UI_PROFILES: dict[str, dict[str, Any]] = {
|
|||
"steering_methods",
|
||||
],
|
||||
"enabledModes": ["plan", "work", "control"],
|
||||
"uiFeatures": {
|
||||
"criticalPathControl": True,
|
||||
},
|
||||
},
|
||||
"initiative.maturity_journey": {
|
||||
"processSteps": list(_MATURITY_PROCESS),
|
||||
|
|
@ -117,6 +145,7 @@ INITIATIVE_UI_PROFILES: dict[str, dict[str, Any]] = {
|
|||
"steering_methods",
|
||||
],
|
||||
"enabledModes": ["plan", "work", "control"],
|
||||
"uiFeatures": {},
|
||||
},
|
||||
"initiative.program": {
|
||||
"processSteps": list(_PROGRAM_PROCESS),
|
||||
|
|
@ -138,6 +167,7 @@ INITIATIVE_UI_PROFILES: dict[str, dict[str, Any]] = {
|
|||
"steering_methods",
|
||||
],
|
||||
"enabledModes": ["plan", "work", "control"],
|
||||
"uiFeatures": {},
|
||||
},
|
||||
"initiative.support_queue": {
|
||||
"processSteps": list(_SUPPORT_QUEUE_PROCESS),
|
||||
|
|
@ -153,6 +183,7 @@ INITIATIVE_UI_PROFILES: dict[str, dict[str, Any]] = {
|
|||
"steering_methods",
|
||||
],
|
||||
"enabledModes": ["plan", "work", "control"],
|
||||
"uiFeatures": {},
|
||||
},
|
||||
"initiative.recurring_program": {
|
||||
"processSteps": list(_MATURITY_PROCESS),
|
||||
|
|
@ -169,6 +200,44 @@ INITIATIVE_UI_PROFILES: dict[str, dict[str, Any]] = {
|
|||
"steering_methods",
|
||||
],
|
||||
"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",
|
||||
description="Reaktive Steuerung, Fristen, Entscheidungen",
|
||||
next_action_strategy_key="default",
|
||||
data_slices=frozenset(
|
||||
{
|
||||
"actions",
|
||||
"blockers",
|
||||
"evidence",
|
||||
"decisions",
|
||||
"reviews",
|
||||
"steering_methods",
|
||||
}
|
||||
),
|
||||
compatible_archetype_keys=frozenset({"initiative.dispute_case"}),
|
||||
)
|
||||
register_stub_method(
|
||||
|
|
@ -65,5 +75,16 @@ def register() -> None:
|
|||
label="Kapitel-Entwicklung",
|
||||
description="Inhaltliche Progression, Reviews",
|
||||
next_action_strategy_key="default",
|
||||
data_slices=frozenset(
|
||||
{
|
||||
"actions",
|
||||
"roadmap",
|
||||
"blockers",
|
||||
"evidence",
|
||||
"decisions",
|
||||
"reviews",
|
||||
"steering_methods",
|
||||
}
|
||||
),
|
||||
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 |
|
||||
| AttentionItem | ◐ | |
|
||||
| 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 |
|
||||
| signals (Snapshot) | ✓ | `snapshot_signals.py` |
|
||||
| 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 |
|
||||
| Execution-Plan (AP-Graph) | ◐ | AP1.16c: Outline + Arbeit-Liste + Action-Detail |
|
||||
| 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 |
|
||||
| Admin-UI | ✗ | |
|
||||
| Team-Modus | ✗ | AP1.9f |
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
**Stand:** 2026-07-12
|
||||
**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)
|
||||
**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,
|
||||
ControlLayout,
|
||||
ControlIndexRedirect,
|
||||
MODE_ROUTE_COMPONENTS,
|
||||
} from './registry/viewRegistry.js'
|
||||
import { OperatingRouteGate } from './registry/OperatingRouteGate.jsx'
|
||||
import { renderModeRoutes } from './registry/renderModeRoutes.jsx'
|
||||
import {
|
||||
WorkspaceRedirect,
|
||||
MyActionsRedirect,
|
||||
|
|
@ -65,90 +64,17 @@ function AppRoutes() {
|
|||
|
||||
<Route path="/work" element={<WorkLayout />}>
|
||||
<Route index element={<WorkIndexRedirect />} />
|
||||
<Route
|
||||
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>
|
||||
}
|
||||
/>
|
||||
{renderModeRoutes('work')}
|
||||
</Route>
|
||||
|
||||
<Route path="/plan" element={<PlanLayout />}>
|
||||
<Route index element={<PlanIndexRedirect />} />
|
||||
<Route path="portfolio" element={<MODE_ROUTE_COMPONENTS.planPortfolio />} />
|
||||
<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 />} />
|
||||
{renderModeRoutes('plan')}
|
||||
</Route>
|
||||
|
||||
<Route path="/control" element={<ControlLayout />}>
|
||||
<Route index element={<ControlIndexRedirect />} />
|
||||
<Route path="status" element={<MODE_ROUTE_COMPONENTS.controlStatus />} />
|
||||
<Route
|
||||
path="plan-ist"
|
||||
element={
|
||||
<OperatingRouteGate>
|
||||
<MODE_ROUTE_COMPONENTS.controlPlanIst />
|
||||
</OperatingRouteGate>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="journey"
|
||||
element={
|
||||
<OperatingRouteGate>
|
||||
<MODE_ROUTE_COMPONENTS.controlJourney />
|
||||
</OperatingRouteGate>
|
||||
}
|
||||
/>
|
||||
{renderModeRoutes('control')}
|
||||
</Route>
|
||||
|
||||
<Route path="/projects/:projectId" element={<ProjectObjectPage />} />
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ import {
|
|||
applySliceResults,
|
||||
loadOperatingSlices,
|
||||
} from '../registry/operatingSliceLoaders.js'
|
||||
import { resolveOperatingProfileFromInput } from '../registry/resolveOperatingProfile.js'
|
||||
|
||||
const InitiativeOperationsContext = createContext(null)
|
||||
|
||||
|
|
@ -158,6 +159,22 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
[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 } = {}) => {
|
||||
if (sessionLoading || !id) return
|
||||
if (!silent) setLoading(true)
|
||||
|
|
@ -319,7 +336,8 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
setMethodBusy(true)
|
||||
try {
|
||||
await updateInitiativeSteeringMethod(id, methodKey)
|
||||
setSteeringSnapshot(await getInitiativeSteeringSnapshot(id))
|
||||
await refreshOperatingContext()
|
||||
await refreshOm(['steering_snapshot', 'steering_methods'])
|
||||
} catch (err) {
|
||||
setSteeringSnapshotError(err.message)
|
||||
} finally {
|
||||
|
|
@ -331,7 +349,8 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
setMethodBusy(true)
|
||||
try {
|
||||
await updateInitiativeMethodProfile(id, methodProfileKey || null)
|
||||
setSteeringSnapshot(await getInitiativeSteeringSnapshot(id))
|
||||
await refreshOperatingContext()
|
||||
await refreshOm(['steering_snapshot', 'steering_methods'])
|
||||
} catch (err) {
|
||||
setSteeringSnapshotError(err.message)
|
||||
} finally {
|
||||
|
|
@ -354,7 +373,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
assigned_actor_ids: assigned,
|
||||
})
|
||||
setShowActionForm(false)
|
||||
await load()
|
||||
await refreshOm(['actions'])
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
|
|
@ -383,7 +402,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
await setActionAssignments(actionId, payload.assigned_actor_ids || [])
|
||||
}
|
||||
setEditingActionId(null)
|
||||
await load()
|
||||
await refreshOm(['actions'])
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
|
|
@ -410,7 +429,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
if (!capabilities.has('kairo.action.manage')) return
|
||||
try {
|
||||
await updateAction(action.id, { status })
|
||||
await load()
|
||||
await refreshOm(['actions'])
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
}
|
||||
|
|
@ -420,7 +439,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
setFormBusy(true)
|
||||
try {
|
||||
await createInitiativeBlocker(id, body)
|
||||
await load()
|
||||
await refreshOm(['blockers', 'actions'])
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
|
|
@ -437,7 +456,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
action_id: actionId,
|
||||
set_action_blocked: true,
|
||||
})
|
||||
await load()
|
||||
await refreshOm(['blockers', 'actions'])
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
|
|
@ -448,7 +467,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
async function handleBlockerStatus(blockerId, status) {
|
||||
try {
|
||||
await updateBlocker(blockerId, { status })
|
||||
await load()
|
||||
await refreshOm(['blockers', 'actions'])
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
}
|
||||
|
|
@ -457,7 +476,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
async function handleDeleteBlocker(blockerId) {
|
||||
try {
|
||||
await deleteBlocker(blockerId)
|
||||
await load()
|
||||
await refreshOm(['blockers', 'actions'])
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
}
|
||||
|
|
@ -469,7 +488,11 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
try {
|
||||
await updateInitiative(id, core)
|
||||
await saveInitiativeDynamicFields(id, dynamicFields)
|
||||
await load()
|
||||
await refreshOm(['initiative'])
|
||||
if (core.archetype_key) {
|
||||
await refreshOperatingContext()
|
||||
await load({ silent: true })
|
||||
}
|
||||
return true
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
|
|
@ -483,7 +506,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
setFormBusy(true)
|
||||
try {
|
||||
await createInitiativeBacklogItem(id, body)
|
||||
await load()
|
||||
await refreshOm(['backlog'])
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
|
|
@ -495,7 +518,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
setFormBusy(true)
|
||||
try {
|
||||
await updateBacklogItem(itemId, body)
|
||||
await load()
|
||||
await refreshOm(['backlog'])
|
||||
return true
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
|
|
@ -511,7 +534,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
roadmap_item_id: roadmapItemId || undefined,
|
||||
clear_roadmap_item: !roadmapItemId,
|
||||
})
|
||||
await load()
|
||||
await refreshOm(['backlog'])
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
}
|
||||
|
|
@ -520,7 +543,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
async function handleBacklogStatus(itemId, status) {
|
||||
try {
|
||||
await updateBacklogItem(itemId, { status })
|
||||
await load()
|
||||
await refreshOm(['backlog'])
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
}
|
||||
|
|
@ -587,7 +610,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
setFormBusy(true)
|
||||
try {
|
||||
const created = await createWorkCycle(id, body)
|
||||
await load({ silent: true })
|
||||
await refreshOm(['work_cycles', 'actions'])
|
||||
if (created?.id) setSelectedWorkCycleId(created.id)
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
|
|
@ -600,7 +623,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
setFormBusy(true)
|
||||
try {
|
||||
await activateWorkCycle(id, workCycleId)
|
||||
await load({ silent: true })
|
||||
await refreshOm(['work_cycles', 'actions'])
|
||||
setSelectedWorkCycleId(workCycleId)
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
|
|
@ -613,7 +636,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
setFormBusy(true)
|
||||
try {
|
||||
await completeWorkCycle(id, workCycleId)
|
||||
await load({ silent: true })
|
||||
await refreshOm(['work_cycles', 'actions'])
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
|
|
@ -635,7 +658,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
}
|
||||
}
|
||||
await completeWorkCycle(id, workCycleId)
|
||||
await load({ silent: true })
|
||||
await refreshOm(['work_cycles', 'actions'])
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
throw err
|
||||
|
|
@ -648,7 +671,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
setFormBusy(true)
|
||||
try {
|
||||
await reactivateWorkCycle(id, workCycleId)
|
||||
await load({ silent: true })
|
||||
await refreshOm(['work_cycles', 'actions'])
|
||||
setSelectedWorkCycleId(workCycleId)
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
|
|
@ -660,7 +683,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
async function handleDeleteBacklog(itemId) {
|
||||
try {
|
||||
await deleteBacklogItem(itemId)
|
||||
await load()
|
||||
await refreshOm(['backlog'])
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
}
|
||||
|
|
@ -670,7 +693,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
setFormBusy(true)
|
||||
try {
|
||||
const created = await createInitiativeRoadmapItem(id, body)
|
||||
await load()
|
||||
await refreshOm(['roadmap'])
|
||||
return created
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
|
|
@ -683,7 +706,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
async function handleRoadmapItemStatus(itemId, status) {
|
||||
try {
|
||||
await updateRoadmapItem(itemId, { status })
|
||||
await load()
|
||||
await refreshOm(['roadmap'])
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
}
|
||||
|
|
@ -692,7 +715,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
async function handleUpdateRoadmapItem(itemId, body) {
|
||||
try {
|
||||
await updateRoadmapItem(itemId, body)
|
||||
await load()
|
||||
await refreshOm(['roadmap'])
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
}
|
||||
|
|
@ -708,7 +731,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
status: 'accepted',
|
||||
})
|
||||
await verifyRoadmapItemReached(itemId)
|
||||
await load()
|
||||
await refreshOm(['roadmap', 'steering_snapshot'])
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
|
|
@ -720,7 +743,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
setFormBusy(true)
|
||||
try {
|
||||
await verifyRoadmapItemReached(itemId)
|
||||
await load()
|
||||
await refreshOm(['roadmap', 'steering_snapshot'])
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
|
|
@ -731,7 +754,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
async function handleDeleteRoadmapItem(itemId) {
|
||||
try {
|
||||
await deleteRoadmapItem(itemId)
|
||||
await load()
|
||||
await refreshOm(['roadmap'])
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
}
|
||||
|
|
@ -812,7 +835,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
await Promise.all(
|
||||
patches.map((patch) => updateBacklogItem(patch.id, { sort_order: patch.sort_order })),
|
||||
)
|
||||
await load()
|
||||
await refreshOm(['backlog'])
|
||||
return true
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
|
|
@ -830,7 +853,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
await Promise.all(
|
||||
patches.map((patch) => updateRoadmapItem(patch.id, { sort_order: patch.sort_order })),
|
||||
)
|
||||
await load()
|
||||
await refreshOm(['roadmap'])
|
||||
return true
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
|
|
@ -849,7 +872,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
setFormBusy(true)
|
||||
try {
|
||||
await createInitiativeEvidence(id, body)
|
||||
await load()
|
||||
await refreshOm(['evidence'])
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
|
|
@ -860,7 +883,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
async function handleEvidenceStatus(evidenceId, status) {
|
||||
try {
|
||||
await updateEvidence(evidenceId, { status })
|
||||
await load()
|
||||
await refreshOm(['evidence'])
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
}
|
||||
|
|
@ -869,7 +892,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
async function handleDeleteEvidence(evidenceId) {
|
||||
try {
|
||||
await deleteEvidence(evidenceId)
|
||||
await load()
|
||||
await refreshOm(['evidence'])
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
}
|
||||
|
|
@ -879,7 +902,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
setFormBusy(true)
|
||||
try {
|
||||
await createInitiativeDecision(id, body)
|
||||
await load()
|
||||
await refreshOm(['decisions'])
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
|
|
@ -890,7 +913,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
async function handleDecisionStatus(decisionId, status) {
|
||||
try {
|
||||
await updateDecision(decisionId, { status })
|
||||
await load()
|
||||
await refreshOm(['decisions'])
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
}
|
||||
|
|
@ -899,7 +922,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
async function handleDeleteDecision(decisionId) {
|
||||
try {
|
||||
await deleteDecision(decisionId)
|
||||
await load()
|
||||
await refreshOm(['decisions'])
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
}
|
||||
|
|
@ -909,7 +932,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
setFormBusy(true)
|
||||
try {
|
||||
await createInitiativeReview(id, body)
|
||||
await load()
|
||||
await refreshOm(['reviews'])
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
|
|
@ -920,7 +943,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
async function handleReviewStatus(reviewId, status) {
|
||||
try {
|
||||
await updateReview(reviewId, { status })
|
||||
await load()
|
||||
await refreshOm(['reviews'])
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
}
|
||||
|
|
@ -929,7 +952,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
async function handleDeleteReview(reviewId) {
|
||||
try {
|
||||
await deleteReview(reviewId)
|
||||
await load()
|
||||
await refreshOm(['reviews'])
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
}
|
||||
|
|
@ -939,7 +962,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
setFormBusy(true)
|
||||
try {
|
||||
await createInitiativeRecurring(id, body)
|
||||
await load()
|
||||
await refreshOm(['recurring'])
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
|
|
@ -950,7 +973,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
async function handleRecurringStatus(recurringId, status) {
|
||||
try {
|
||||
await updateRecurring(recurringId, { status })
|
||||
await load()
|
||||
await refreshOm(['recurring'])
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
}
|
||||
|
|
@ -959,12 +982,23 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
async function handleDeleteRecurring(recurringId) {
|
||||
try {
|
||||
await deleteRecurring(recurringId)
|
||||
await load()
|
||||
await refreshOm(['recurring'])
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
}
|
||||
}
|
||||
|
||||
const operatingProfile = useMemo(
|
||||
() =>
|
||||
resolveOperatingProfileFromInput({
|
||||
operatingContext,
|
||||
hasActiveSprint: Boolean(activeWorkCycle?.id),
|
||||
}),
|
||||
[operatingContext, activeWorkCycle?.id],
|
||||
)
|
||||
|
||||
const uiFeatures = operatingProfile.uiFeatures || {}
|
||||
|
||||
const value = {
|
||||
initiativeId: id,
|
||||
initiative,
|
||||
|
|
@ -996,6 +1030,8 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
steeringMethods,
|
||||
operatingContext,
|
||||
dataSlices: operatingContext?.data_slices || [],
|
||||
uiFeatures,
|
||||
operatingProfile,
|
||||
methodBusy,
|
||||
loading,
|
||||
error,
|
||||
|
|
@ -1015,6 +1051,8 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
reloadActors: actorsState.reload,
|
||||
reload: load,
|
||||
reloadSlices,
|
||||
refreshOm,
|
||||
refreshOperatingContext,
|
||||
handleMethodChange,
|
||||
handleMethodProfileChange,
|
||||
handleCreateAction,
|
||||
|
|
|
|||
|
|
@ -5,14 +5,6 @@ import { SteeringSnapshotPanel } from '../../components/SteeringSnapshotPanel.js
|
|||
import { CriticalPathPanel } from '../../components/CriticalPathPanel.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() {
|
||||
const {
|
||||
initiativeId,
|
||||
|
|
@ -26,10 +18,11 @@ export function InitiativeOverviewPage() {
|
|||
methodBusy,
|
||||
capabilities,
|
||||
handleMethodChange,
|
||||
uiFeatures,
|
||||
} = useInitiativeOperations()
|
||||
|
||||
const counts = steeringSnapshot?.counts || {}
|
||||
const showCriticalPath = usesCriticalPathControl(initiative, steeringSnapshot)
|
||||
const showCriticalPath = Boolean(uiFeatures.criticalPathControl)
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import {
|
|||
CONTROL_NAV_ITEMS,
|
||||
resolveControlNavActiveKey,
|
||||
} from '../../config/controlNav.js'
|
||||
import { useProgramScope } from '../../context/ProgramScopeContext.jsx'
|
||||
import { useMethodUiContext } from '../../hooks/useMethodUiContext.js'
|
||||
|
||||
export function ControlLayout() {
|
||||
return (
|
||||
|
|
@ -29,5 +31,13 @@ export function ControlLayout() {
|
|||
}
|
||||
|
||||
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 { useProgramScope } from '../../context/ProgramScopeContext.jsx'
|
||||
import { getInitiativeOperatingContext } from '../../api/initiatives.js'
|
||||
import { getActiveWorkCycle } from '../../api/workCycles.js'
|
||||
import { resolveOperatingProfileFromInput } from '../../registry/resolveOperatingProfile.js'
|
||||
import { useMethodUiContext } from '../../hooks/useMethodUiContext.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() {
|
||||
const { initiativeId, hrefWithScope } = useProgramScope()
|
||||
const [target, setTarget] = useState(null)
|
||||
|
||||
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])
|
||||
const { planDefaultRoute, loading, operatingContext } = useMethodUiContext()
|
||||
|
||||
if (!initiativeId) {
|
||||
return <Navigate to="/plan/portfolio" replace />
|
||||
}
|
||||
|
||||
if (!target) {
|
||||
if (loading && !operatingContext) {
|
||||
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 { useEffect, useState } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { ModeAreaShell } from '../../components/ModeAreaShell.jsx'
|
||||
import { ModeAreaNav } from '../../components/ModeAreaNav.jsx'
|
||||
import { ModeShell } from '../../components/ModeShell.jsx'
|
||||
import { WORK_NAV_ITEMS, resolveWorkNavActiveKey } from '../../config/workNav.js'
|
||||
import { resolveWorkNavItems } from '../../config/methodUiDefaults.js'
|
||||
import { useProgramScope } from '../../context/ProgramScopeContext.jsx'
|
||||
import { getInitiative } from '../../api/initiatives.js'
|
||||
import { getActiveWorkCycle } from '../../api/workCycles.js'
|
||||
import { resolveMethodUiDefaults } from '../../config/methodUiDefaults.js'
|
||||
import { useMethodUiContext } from '../../hooks/useMethodUiContext.js'
|
||||
import { resolveWorkNavItemsFromProfile } from '../../registry/resolveOperatingProfile.js'
|
||||
|
||||
function WorkAreaNav() {
|
||||
const { initiativeId } = useProgramScope()
|
||||
const [navItems, setNavItems] = useState(WORK_NAV_ITEMS)
|
||||
const { operatingContext, hasActiveSprint, loading } = useMethodUiContext()
|
||||
|
||||
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])
|
||||
const navItems = useMemo(() => {
|
||||
if (!initiativeId) return WORK_NAV_ITEMS
|
||||
if (loading && !operatingContext) return WORK_NAV_ITEMS
|
||||
return resolveWorkNavItemsFromProfile({ operatingContext, hasActiveSprint })
|
||||
}, [initiativeId, loading, operatingContext, hasActiveSprint])
|
||||
|
||||
return (
|
||||
<ModeAreaNav
|
||||
|
|
@ -63,33 +40,15 @@ export function WorkLayout() {
|
|||
|
||||
export function WorkIndexRedirect() {
|
||||
const { initiativeId, hrefWithScope } = useProgramScope()
|
||||
const [target, setTarget] = useState('/work/today')
|
||||
const { workDefaultRoute, loading, operatingContext } = useMethodUiContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!initiativeId) {
|
||||
setTarget('/work/today')
|
||||
return undefined
|
||||
return <Navigate to="/work/today" replace />
|
||||
}
|
||||
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 { Link } from 'react-router-dom'
|
||||
import { RequireInitiativeScope } from '../../components/RequireInitiativeScope.jsx'
|
||||
import { ScopedInitiativeProvider } from '../../layout/ScopedInitiativeProvider.jsx'
|
||||
import { InitiativeActionsHub } from '../../components/InitiativeActionsHub.jsx'
|
||||
import { NextActionWidget } from '../../widgets/NextActionWidget.jsx'
|
||||
import { LoadingState } from '../../components/LoadingState.jsx'
|
||||
|
|
@ -43,6 +42,7 @@ function WorkSprintInner() {
|
|||
initiative,
|
||||
operatingContext,
|
||||
dataSlices,
|
||||
uiFeatures,
|
||||
steeringSnapshot,
|
||||
steeringSnapshotLoading,
|
||||
visibleActions,
|
||||
|
|
@ -50,13 +50,20 @@ function WorkSprintInner() {
|
|||
} = ops
|
||||
|
||||
const supportsWorkCycles = dataSlices.includes('work_cycles')
|
||||
const isContinuousProduct = operatingContext?.method_key === 'continuous_product'
|
||||
const isContinuousProductMode = uiFeatures.continuousProductWorkMode && supportsWorkCycles
|
||||
const loadSteeringSnapshot = uiFeatures.steeringSnapshotOnWorkSprint
|
||||
|
||||
useEffect(() => {
|
||||
if (initiativeId && !steeringSnapshot && !steeringSnapshotLoading) {
|
||||
if (initiativeId && loadSteeringSnapshot && !steeringSnapshot && !steeringSnapshotLoading) {
|
||||
reloadSlices(['steering_snapshot'])
|
||||
}
|
||||
}, [initiativeId, steeringSnapshot, steeringSnapshotLoading, reloadSlices])
|
||||
}, [
|
||||
initiativeId,
|
||||
loadSteeringSnapshot,
|
||||
steeringSnapshot,
|
||||
steeringSnapshotLoading,
|
||||
reloadSlices,
|
||||
])
|
||||
|
||||
const defaultWorkCycleId = activeWorkCycle?.id || ''
|
||||
|
||||
|
|
@ -74,7 +81,7 @@ function WorkSprintInner() {
|
|||
return <LoadingState message="Lade Sprint-Backlog …" />
|
||||
}
|
||||
|
||||
const isProductArchetype = isContinuousProduct && supportsWorkCycles
|
||||
const isProductArchetype = isContinuousProductMode
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -86,7 +93,7 @@ function WorkSprintInner() {
|
|||
<h2>Product-Ist (ohne aktiven Sprint)</h2>
|
||||
<p className="section-lead muted">
|
||||
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{' '}
|
||||
<Link to={hrefWithScope('/plan/sprint')} className="link-inline">
|
||||
Plan → Sprint
|
||||
|
|
@ -208,10 +215,8 @@ function WorkSprintInner() {
|
|||
|
||||
export function WorkSprintPage() {
|
||||
return (
|
||||
<RequireInitiativeScope lead="Sprint-Backlog ist vorhabenspezifisch — wähle ein Product-Vorhaben im Scope.">
|
||||
<ScopedInitiativeProvider>
|
||||
<RequireInitiativeScope lead="Sprint-Backlog ist vorhabenspezifisch — wähle ein passendes Vorhaben im Scope.">
|
||||
<WorkSprintInner />
|
||||
</ScopedInitiativeProvider>
|
||||
</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 { WORK_NAV_ITEMS } from '../config/workNav.js'
|
||||
|
|
@ -7,6 +7,10 @@ import {
|
|||
resolveMethodUiDefaults,
|
||||
resolveActiveProcessStepKey as resolveActiveProcessStepKeyFallback,
|
||||
} from '../config/methodUiDefaults.js'
|
||||
import {
|
||||
isRouteAllowedForMode,
|
||||
resolveRedirectForModeRoute,
|
||||
} from './modeRouteRegistry.js'
|
||||
|
||||
/**
|
||||
* @typedef {import('../config/methodUiDefaults.js').ProcessStep} ProcessStep
|
||||
|
|
@ -16,7 +20,7 @@ import {
|
|||
* archetype_key?: string | null,
|
||||
* method_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[],
|
||||
* method_capabilities?: object,
|
||||
* }} OperatingContextResponse
|
||||
|
|
@ -35,32 +39,49 @@ const EMPTY_PROFILE = {
|
|||
controlDefaultRoute: '/control/status',
|
||||
planOutlineKeys: 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 {{ hasActiveSprint?: boolean }} [options]
|
||||
* @returns {MethodUiDefaults & { dataSlices: string[] }}
|
||||
* @returns {MethodUiDefaults & { dataSlices: string[], uiFeatures: Record<string, boolean> }}
|
||||
*/
|
||||
export function resolveOperatingProfile(context, options = {}) {
|
||||
if (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 {
|
||||
processSteps: profile.processSteps || [],
|
||||
planDefaultRoute: profile.planDefaultRoute || EMPTY_PROFILE.planDefaultRoute,
|
||||
workDefaultRoute,
|
||||
workDefaultRoute: resolveWorkDefaultRoute(profile, options),
|
||||
controlDefaultRoute:
|
||||
profile.controlDefaultRoute || EMPTY_PROFILE.controlDefaultRoute,
|
||||
planOutlineKeys: profile.planOutlineKeys ?? null,
|
||||
workNavKeys: profile.workNavKeys ?? null,
|
||||
dataSlices: context.data_slices || profile.dataSlices || [],
|
||||
uiFeatures: mergeUiFeatures(profile.uiFeatures, context?.method_key),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -72,12 +93,13 @@ export function resolveOperatingProfile(context, options = {}) {
|
|||
return {
|
||||
...fallback,
|
||||
dataSlices: context?.data_slices || [],
|
||||
uiFeatures: mergeUiFeatures({}, context?.method_key),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveOperatingProfileInput} input
|
||||
* @returns {MethodUiDefaults & { dataSlices: string[] }}
|
||||
* @returns {MethodUiDefaults & { dataSlices: string[], uiFeatures: Record<string, boolean> }}
|
||||
*/
|
||||
export function resolveOperatingProfileFromInput(input = {}) {
|
||||
if (input.operatingContext) {
|
||||
|
|
@ -90,7 +112,7 @@ export function resolveOperatingProfileFromInput(input = {}) {
|
|||
methodKey: input.methodKey || null,
|
||||
hasActiveSprint: input.hasActiveSprint,
|
||||
})
|
||||
return { ...fallback, dataSlices: [] }
|
||||
return { ...fallback, dataSlices: [], uiFeatures: {} }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -132,23 +154,10 @@ export function resolveActiveProcessStepKey(pathname, steps) {
|
|||
*/
|
||||
export function isRouteAllowedForProfile(routePath, input = {}) {
|
||||
const profile = resolveOperatingProfileFromInput(input)
|
||||
const normalized = (routePath || '').replace(/\/$/, '')
|
||||
|
||||
if (normalized.startsWith('/plan/sprint') || normalized.startsWith('/work/sprint')) {
|
||||
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
|
||||
return isRouteAllowedForMode(routePath, {
|
||||
operatingContext: input.operatingContext,
|
||||
dataSlices: profile.dataSlices,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -157,14 +166,12 @@ export function isRouteAllowedForProfile(routePath, input = {}) {
|
|||
* @returns {string}
|
||||
*/
|
||||
export function resolveRedirectForDisallowedRoute(routePath, input = {}) {
|
||||
if (input.operatingContext) {
|
||||
return resolveRedirectForModeRoute(routePath, input)
|
||||
}
|
||||
const profile = resolveOperatingProfileFromInput(input)
|
||||
const normalized = (routePath || '').replace(/\/$/, '')
|
||||
|
||||
if (normalized.startsWith('/work/')) {
|
||||
return profile.workDefaultRoute
|
||||
}
|
||||
if (normalized.startsWith('/control/')) {
|
||||
return profile.controlDefaultRoute
|
||||
}
|
||||
if (normalized.startsWith('/work/')) return profile.workDefaultRoute
|
||||
if (normalized.startsWith('/control/')) return profile.controlDefaultRoute
|
||||
return profile.planDefaultRoute
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ const PRODUCT_CONTEXT = {
|
|||
],
|
||||
planDefaultRoute: '/plan/inbox',
|
||||
workDefaultRoute: '/work/sprint',
|
||||
workDefaultRouteWithoutActiveWorkCycle: '/work/today',
|
||||
controlDefaultRoute: '/control/status',
|
||||
planOutlineKeys: ['profile', 'inbox', 'sprint', 'gates'],
|
||||
workNavKeys: ['sprint', 'today', 'mine'],
|
||||
|
|
@ -73,4 +74,25 @@ describe('resolveOperatingProfile', () => {
|
|||
const input = { operatingContext: PRODUCT_CONTEXT }
|
||||
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,
|
||||
}
|
||||
|
||||
/** Modus-Routen mit Slice-Gating (AP2.3d). */
|
||||
export const MODE_ROUTES = [
|
||||
{ path: 'today', componentKey: 'workToday', requiredSlices: ['actions'] },
|
||||
{ path: 'mine', componentKey: 'workMine', requiredSlices: ['actions'] },
|
||||
/** 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,
|
||||
},
|
||||
{ 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'] },
|
||||
],
|
||||
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'] },
|
||||
{ path: 'status', componentKey: 'controlStatus', requiredSlices: [] },
|
||||
{ path: 'plan-ist', componentKey: 'controlPlanIst', requiredSlices: ['roadmap'] },
|
||||
{ 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 = [
|
||||
...MODE_ROUTE_GROUPS.work,
|
||||
...MODE_ROUTE_GROUPS.plan,
|
||||
...MODE_ROUTE_GROUPS.control,
|
||||
]
|
||||
|
||||
export {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user