/** * 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' 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, workDefaultRouteWithoutActiveWorkCycle?: string }> | null, * data_slices?: string[], * method_capabilities?: 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: {}, } /** 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[], uiFeatures: Record }} */ 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: context.data_slices || profile.dataSlices || [], uiFeatures: mergeUiFeatures(profile.uiFeatures, context?.method_key), } } const fallback = resolveMethodUiDefaults({ archetypeKey: context?.archetype_key || null, methodKey: context?.method_key || null, hasActiveSprint: options.hasActiveSprint, }) return { ...fallback, dataSlices: context?.data_slices || [], uiFeatures: mergeUiFeatures({}, context?.method_key), } } /** * @param {ResolveOperatingProfileInput} input * @returns {MethodUiDefaults & { dataSlices: string[], uiFeatures: Record }} */ 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: {} } } /** * @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) 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 }