2026-07-25 10:26:37 +02:00
|
|
|
/**
|
2026-07-25 11:27:59 +02:00
|
|
|
* AP2.3b/f / AP2.4 — Operating Profile Resolver (API-first, methodUiDefaults nur als Fallback).
|
2026-07-25 10:26:37 +02:00
|
|
|
*/
|
|
|
|
|
import { PLAN_OUTLINE_NODES } from '../plan/planOutlineNodes.js'
|
|
|
|
|
import { WORK_NAV_ITEMS } from '../config/workNav.js'
|
|
|
|
|
import {
|
|
|
|
|
resolveMethodUiDefaults,
|
|
|
|
|
resolveActiveProcessStepKey as resolveActiveProcessStepKeyFallback,
|
|
|
|
|
} from '../config/methodUiDefaults.js'
|
2026-07-25 10:52:12 +02:00
|
|
|
import {
|
|
|
|
|
isRouteAllowedForMode,
|
|
|
|
|
resolveRedirectForModeRoute,
|
|
|
|
|
} from './modeRouteRegistry.js'
|
2026-07-25 10:26:37 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @typedef {import('../config/methodUiDefaults.js').ProcessStep} ProcessStep
|
|
|
|
|
* @typedef {import('../config/methodUiDefaults.js').MethodUiDefaults} MethodUiDefaults
|
|
|
|
|
* @typedef {{
|
|
|
|
|
* initiative_id?: string,
|
|
|
|
|
* archetype_key?: string | null,
|
|
|
|
|
* method_key?: string | null,
|
|
|
|
|
* method_profile_key?: string | null,
|
2026-07-25 11:27:59 +02:00
|
|
|
* ui_profile?: Partial<MethodUiDefaults & { workDefaultRouteWithoutActiveWorkCycle?: string }> | null,
|
2026-07-25 10:26:37 +02:00
|
|
|
* data_slices?: string[],
|
2026-07-25 11:27:59 +02:00
|
|
|
* om_capabilities?: string[],
|
2026-07-25 10:26:37 +02:00
|
|
|
* method_capabilities?: object,
|
2026-07-25 11:27:59 +02:00
|
|
|
* steering_elements?: string[],
|
|
|
|
|
* ui_features?: Record<string, boolean>,
|
|
|
|
|
* graph_profile?: { enforce_gate_blocking?: boolean, emphasize_fulfillment?: boolean },
|
|
|
|
|
* compatible_methods?: object[],
|
2026-07-25 10:26:37 +02:00
|
|
|
* }} OperatingContextResponse
|
|
|
|
|
* @typedef {{
|
|
|
|
|
* archetypeKey?: string | null,
|
|
|
|
|
* methodKey?: string | null,
|
|
|
|
|
* hasActiveSprint?: boolean,
|
|
|
|
|
* operatingContext?: OperatingContextResponse | null,
|
|
|
|
|
* }} ResolveOperatingProfileInput
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
const EMPTY_PROFILE = {
|
|
|
|
|
processSteps: [],
|
|
|
|
|
planDefaultRoute: '/plan/profile',
|
|
|
|
|
workDefaultRoute: '/work/today',
|
|
|
|
|
controlDefaultRoute: '/control/status',
|
|
|
|
|
planOutlineKeys: null,
|
|
|
|
|
workNavKeys: null,
|
2026-07-25 10:52:12 +02:00
|
|
|
uiFeatures: {},
|
2026-07-25 11:27:59 +02:00
|
|
|
steeringElements: [],
|
2026-07-25 10:52:12 +02:00
|
|
|
}
|
|
|
|
|
|
2026-07-25 11:27:59 +02:00
|
|
|
function resolveUiFeatures(context, profileFeatures) {
|
|
|
|
|
if (context?.ui_features && Object.keys(context.ui_features).length) {
|
|
|
|
|
return { ...(context.ui_features || {}) }
|
|
|
|
|
}
|
|
|
|
|
return { ...(profileFeatures || {}) }
|
2026-07-25 10:52:12 +02:00
|
|
|
}
|
|
|
|
|
|
2026-07-25 11:27:59 +02:00
|
|
|
function resolveSteeringElements(context) {
|
|
|
|
|
if (Array.isArray(context?.steering_elements)) {
|
|
|
|
|
return [...context.steering_elements]
|
|
|
|
|
}
|
|
|
|
|
return []
|
2026-07-25 10:52:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveWorkDefaultRoute(profile, options = {}) {
|
|
|
|
|
const base = profile.workDefaultRoute || EMPTY_PROFILE.workDefaultRoute
|
|
|
|
|
if (!options.hasActiveSprint && profile.workDefaultRouteWithoutActiveWorkCycle) {
|
|
|
|
|
return profile.workDefaultRouteWithoutActiveWorkCycle
|
|
|
|
|
}
|
|
|
|
|
return base
|
2026-07-25 10:26:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {OperatingContextResponse | null | undefined} context
|
|
|
|
|
* @param {{ hasActiveSprint?: boolean }} [options]
|
|
|
|
|
*/
|
|
|
|
|
export function resolveOperatingProfile(context, options = {}) {
|
|
|
|
|
if (context?.ui_profile) {
|
|
|
|
|
const profile = context.ui_profile
|
|
|
|
|
return {
|
|
|
|
|
processSteps: profile.processSteps || [],
|
|
|
|
|
planDefaultRoute: profile.planDefaultRoute || EMPTY_PROFILE.planDefaultRoute,
|
2026-07-25 10:52:12 +02:00
|
|
|
workDefaultRoute: resolveWorkDefaultRoute(profile, options),
|
2026-07-25 10:26:37 +02:00
|
|
|
controlDefaultRoute:
|
|
|
|
|
profile.controlDefaultRoute || EMPTY_PROFILE.controlDefaultRoute,
|
|
|
|
|
planOutlineKeys: profile.planOutlineKeys ?? null,
|
|
|
|
|
workNavKeys: profile.workNavKeys ?? null,
|
|
|
|
|
dataSlices: context.data_slices || profile.dataSlices || [],
|
2026-07-25 11:27:59 +02:00
|
|
|
uiFeatures: resolveUiFeatures(context, profile.uiFeatures),
|
|
|
|
|
steeringElements: resolveSteeringElements(context),
|
2026-07-25 10:26:37 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fallback = resolveMethodUiDefaults({
|
|
|
|
|
archetypeKey: context?.archetype_key || null,
|
|
|
|
|
methodKey: context?.method_key || null,
|
|
|
|
|
hasActiveSprint: options.hasActiveSprint,
|
|
|
|
|
})
|
|
|
|
|
return {
|
|
|
|
|
...fallback,
|
|
|
|
|
dataSlices: context?.data_slices || [],
|
2026-07-25 11:27:59 +02:00
|
|
|
uiFeatures: resolveUiFeatures(context, {}),
|
|
|
|
|
steeringElements: resolveSteeringElements(context),
|
2026-07-25 10:26:37 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {ResolveOperatingProfileInput} input
|
|
|
|
|
*/
|
|
|
|
|
export function resolveOperatingProfileFromInput(input = {}) {
|
|
|
|
|
if (input.operatingContext) {
|
|
|
|
|
return resolveOperatingProfile(input.operatingContext, {
|
|
|
|
|
hasActiveSprint: input.hasActiveSprint,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
const fallback = resolveMethodUiDefaults({
|
|
|
|
|
archetypeKey: input.archetypeKey || null,
|
|
|
|
|
methodKey: input.methodKey || null,
|
|
|
|
|
hasActiveSprint: input.hasActiveSprint,
|
|
|
|
|
})
|
2026-07-25 11:27:59 +02:00
|
|
|
return { ...fallback, dataSlices: [], uiFeatures: {}, steeringElements: [] }
|
2026-07-25 10:26:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {ResolveOperatingProfileInput} input
|
|
|
|
|
* @returns {typeof PLAN_OUTLINE_NODES}
|
|
|
|
|
*/
|
|
|
|
|
export function resolvePlanOutlineNodesFromProfile(input = {}) {
|
|
|
|
|
const { planOutlineKeys } = resolveOperatingProfileFromInput(input)
|
|
|
|
|
if (!planOutlineKeys) return PLAN_OUTLINE_NODES
|
|
|
|
|
const allowed = new Set(planOutlineKeys)
|
|
|
|
|
return PLAN_OUTLINE_NODES.filter((node) => allowed.has(node.key))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {ResolveOperatingProfileInput} input
|
|
|
|
|
* @returns {typeof WORK_NAV_ITEMS}
|
|
|
|
|
*/
|
|
|
|
|
export function resolveWorkNavItemsFromProfile(input = {}) {
|
|
|
|
|
const { workNavKeys } = resolveOperatingProfileFromInput(input)
|
|
|
|
|
if (!workNavKeys) return WORK_NAV_ITEMS
|
|
|
|
|
const order = new Map(workNavKeys.map((key, index) => [key, index]))
|
|
|
|
|
return WORK_NAV_ITEMS.filter((item) => order.has(item.key)).sort(
|
|
|
|
|
(a, b) => (order.get(a.key) ?? 0) - (order.get(b.key) ?? 0),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {string} pathname
|
|
|
|
|
* @param {ProcessStep[]} steps
|
|
|
|
|
*/
|
|
|
|
|
export function resolveActiveProcessStepKey(pathname, steps) {
|
|
|
|
|
return resolveActiveProcessStepKeyFallback(pathname, steps)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {string} routePath
|
|
|
|
|
* @param {ResolveOperatingProfileInput} input
|
|
|
|
|
* @returns {boolean}
|
|
|
|
|
*/
|
|
|
|
|
export function isRouteAllowedForProfile(routePath, input = {}) {
|
|
|
|
|
const profile = resolveOperatingProfileFromInput(input)
|
2026-07-25 10:52:12 +02:00
|
|
|
return isRouteAllowedForMode(routePath, {
|
|
|
|
|
operatingContext: input.operatingContext,
|
|
|
|
|
dataSlices: profile.dataSlices,
|
|
|
|
|
})
|
2026-07-25 10:26:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {string} routePath
|
|
|
|
|
* @param {ResolveOperatingProfileInput} input
|
|
|
|
|
* @returns {string}
|
|
|
|
|
*/
|
|
|
|
|
export function resolveRedirectForDisallowedRoute(routePath, input = {}) {
|
2026-07-25 10:52:12 +02:00
|
|
|
if (input.operatingContext) {
|
|
|
|
|
return resolveRedirectForModeRoute(routePath, input)
|
|
|
|
|
}
|
2026-07-25 10:26:37 +02:00
|
|
|
const profile = resolveOperatingProfileFromInput(input)
|
|
|
|
|
const normalized = (routePath || '').replace(/\/$/, '')
|
2026-07-25 10:52:12 +02:00
|
|
|
if (normalized.startsWith('/work/')) return profile.workDefaultRoute
|
|
|
|
|
if (normalized.startsWith('/control/')) return profile.controlDefaultRoute
|
2026-07-25 10:26:37 +02:00
|
|
|
return profile.planDefaultRoute
|
|
|
|
|
}
|