Kairo-Jinkendo/frontend/src/registry/resolveOperatingProfile.js

178 lines
5.9 KiB
JavaScript
Raw Normal View History

/**
* 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<MethodUiDefaults & { uiFeatures?: Record<string, boolean>, 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<string, boolean> }}
*/
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<string, boolean> }}
*/
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
}