diff --git a/frontend/src/pages/initiative/InitiativeInboxPage.jsx b/frontend/src/pages/initiative/InitiativeInboxPage.jsx index cfa4484..32a1f9a 100644 --- a/frontend/src/pages/initiative/InitiativeInboxPage.jsx +++ b/frontend/src/pages/initiative/InitiativeInboxPage.jsx @@ -7,6 +7,7 @@ export function InitiativeInboxPage() { initiative, initiativeId, backlogItems, + dataSlices, roadmapItems, featureParentActions, workCycles, @@ -31,8 +32,6 @@ export function InitiativeInboxPage() { return } - const isProductArchetype = initiative?.archetype_key === 'initiative.product' - return ( <> {error &&

{error}

} @@ -43,7 +42,7 @@ export function InitiativeInboxPage() { featureActions={featureParentActions} workCycles={workCycles} activeWorkCycle={activeWorkCycle} - sprintPlanningEnabled={isProductArchetype} + sprintPlanningEnabled={dataSlices.includes('work_cycles')} canManage={capabilities.has('kairo.backlog.manage')} onCreate={handleCreateBacklog} onUpdate={handleUpdateBacklog} diff --git a/frontend/src/pages/modes/WorkSprintPage.jsx b/frontend/src/pages/modes/WorkSprintPage.jsx index e168de1..1014c85 100644 --- a/frontend/src/pages/modes/WorkSprintPage.jsx +++ b/frontend/src/pages/modes/WorkSprintPage.jsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react' +import { useEffect, useMemo } from 'react' import { Link } from 'react-router-dom' import { RequireInitiativeScope } from '../../components/RequireInitiativeScope.jsx' import { ScopedInitiativeProvider } from '../../layout/ScopedInitiativeProvider.jsx' @@ -41,11 +41,23 @@ function WorkSprintInner() { handleMoveActionToSprint, handleUnplanAction, initiative, + operatingContext, + dataSlices, steeringSnapshot, steeringSnapshotLoading, visibleActions, + reloadSlices, } = ops + const supportsWorkCycles = dataSlices.includes('work_cycles') + const isContinuousProduct = operatingContext?.method_key === 'continuous_product' + + useEffect(() => { + if (initiativeId && !steeringSnapshot && !steeringSnapshotLoading) { + reloadSlices(['steering_snapshot']) + } + }, [initiativeId, steeringSnapshot, steeringSnapshotLoading, reloadSlices]) + const defaultWorkCycleId = activeWorkCycle?.id || '' const createAction = useMemo( @@ -62,7 +74,7 @@ function WorkSprintInner() { return } - const isProductArchetype = initiative?.archetype_key === 'initiative.product' + const isProductArchetype = isContinuousProduct && supportsWorkCycles return ( <> @@ -187,8 +199,8 @@ function WorkSprintInner() { )} - {!activeWorkCycle && !isProductArchetype && ( - + {!activeWorkCycle && !supportsWorkCycles && ( + )} ) diff --git a/frontend/src/registry/OperatingRouteGate.jsx b/frontend/src/registry/OperatingRouteGate.jsx new file mode 100644 index 0000000..37a152c --- /dev/null +++ b/frontend/src/registry/OperatingRouteGate.jsx @@ -0,0 +1,30 @@ +import { Navigate, useLocation } from 'react-router-dom' +import { useProgramScope } from '../context/ProgramScopeContext.jsx' +import { useMethodUiContext } from '../hooks/useMethodUiContext.js' +import { + isRouteAllowedForProfile, + resolveRedirectForDisallowedRoute, +} from './resolveOperatingProfile.js' + +/** + * AP2.3d — Redirect wenn Route für Archetyp/Slice unpassend. + */ +export function OperatingRouteGate({ children }) { + const location = useLocation() + const { hrefWithScope, initiativeId } = useProgramScope() + const { operatingContext, loading } = useMethodUiContext() + + if (!initiativeId || loading) { + return children + } + + const input = { operatingContext } + const path = location.pathname + + if (isRouteAllowedForProfile(path, input)) { + return children + } + + const redirect = resolveRedirectForDisallowedRoute(path, input) + return +} diff --git a/frontend/src/registry/viewRegistry.js b/frontend/src/registry/viewRegistry.js index 4d11663..a298333 100644 --- a/frontend/src/registry/viewRegistry.js +++ b/frontend/src/registry/viewRegistry.js @@ -144,6 +144,38 @@ 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'] }, + { + 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 { WorkLayout, WorkIndexRedirect,