/** * AP2.3b/f / AP2.4 — 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' import { resolveMethodUiDefaults, resolveActiveProcessStepKey as resolveActiveProcessStepKeyFallback, } from '../config/methodUiDefaults.js' import { isRouteAllowedForMode, resolveRedirectForModeRoute, } from './modeRouteRegistry.js' /** * @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, * ui_profile?: Partial | null, * data_slices?: string[], * om_capabilities?: string[], * method_capabilities?: object, * steering_elements?: string[], * ui_features?: Record, * graph_profile?: { enforce_gate_blocking?: boolean, emphasize_fulfillment?: boolean }, * compatible_methods?: object[], * }} 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, uiFeatures: {}, steeringElements: [], } /** * API data_slices haben Vorrang; leeres Array gilt als „fehlend“ (JS-Truthy-Falle). * @param {OperatingContextResponse | null | undefined} context * @returns {string[]} */ export function resolveEffectiveDataSlices(context) { const apiSlices = context?.data_slices if (Array.isArray(apiSlices) && apiSlices.length > 0) { return apiSlices } const profileSlices = context?.ui_profile?.dataSlices if (Array.isArray(profileSlices) && profileSlices.length > 0) { return profileSlices } return Array.isArray(apiSlices) ? apiSlices : [] } function resolveUiFeatures(context, profileFeatures) { if (context?.ui_features && Object.keys(context.ui_features).length) { return { ...(context.ui_features || {}) } } return { ...(profileFeatures || {}) } } function resolveSteeringElements(context) { if (Array.isArray(context?.steering_elements)) { return [...context.steering_elements] } return [] } 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] */ export function resolveOperatingProfile(context, options = {}) { if (context?.ui_profile) { const profile = context.ui_profile return { processSteps: profile.processSteps || [], planDefaultRoute: profile.planDefaultRoute || EMPTY_PROFILE.planDefaultRoute, workDefaultRoute: resolveWorkDefaultRoute(profile, options), controlDefaultRoute: profile.controlDefaultRoute || EMPTY_PROFILE.controlDefaultRoute, planOutlineKeys: profile.planOutlineKeys ?? null, workNavKeys: profile.workNavKeys ?? null, dataSlices: resolveEffectiveDataSlices(context), uiFeatures: resolveUiFeatures(context, profile.uiFeatures), steeringElements: resolveSteeringElements(context), } } const fallback = resolveMethodUiDefaults({ archetypeKey: context?.archetype_key || null, methodKey: context?.method_key || null, hasActiveSprint: options.hasActiveSprint, }) return { ...fallback, dataSlices: resolveEffectiveDataSlices(context), uiFeatures: resolveUiFeatures(context, {}), steeringElements: resolveSteeringElements(context), } } /** * @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, }) return { ...fallback, dataSlices: [], uiFeatures: {}, steeringElements: [] } } /** * @param {ResolveOperatingProfileInput} input * @returns {typeof PLAN_OUTLINE_NODES} */ export function resolvePlanOutlineNodesFromProfile(input = {}) { const { planOutlineKeys } = resolveOperatingProfileFromInput(input) if (!planOutlineKeys) return PLAN_OUTLINE_NODES const order = new Map(planOutlineKeys.map((key, index) => [key, index])) return PLAN_OUTLINE_NODES.filter((node) => order.has(node.key)).sort( (a, b) => (order.get(a.key) ?? 0) - (order.get(b.key) ?? 0), ) } /** * @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) return isRouteAllowedForMode(routePath, { operatingContext: input.operatingContext, dataSlices: profile.dataSlices, }) } /** * @param {string} routePath * @param {ResolveOperatingProfileInput} 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 return profile.planDefaultRoute }