feat(product): Plan-Arbeit fuer Direct-AP-Pfad (ADP P1)
All checks were successful
Deploy Development / deploy (push) Successful in 49s
Test Suite / pytest-backend (push) Successful in 3m45s
Test Suite / lint-backend (push) Successful in 2s
Test Suite / compose-smoke (push) Has been skipped
Test Suite / k6 /api/health Baseline (push) Successful in 19s
Test Suite / playwright-smoke (push) Successful in 12s

Product-Outline enthaelt work; planOutlineKeys-Reihenfolge steuert Sidebar; Product-spezifischer Lead auf Plan-Arbeit.
This commit is contained in:
Lars 2026-07-26 18:25:12 +02:00
parent f7d65dde01
commit dc6f25b673
6 changed files with 25 additions and 9 deletions

View File

@ -83,7 +83,7 @@ INITIATIVE_UI_PROFILES: dict[str, dict[str, Any]] = {
"workDefaultRoute": "/work/sprint", "workDefaultRoute": "/work/sprint",
"workDefaultRouteWithoutActiveWorkCycle": "/work/today", "workDefaultRouteWithoutActiveWorkCycle": "/work/today",
"controlDefaultRoute": "/control/status", "controlDefaultRoute": "/control/status",
"planOutlineKeys": ["profile", "inbox", "sprint", "gates"], "planOutlineKeys": ["profile", "inbox", "work", "sprint", "gates"],
"workNavKeys": ["sprint", "today", "mine"], "workNavKeys": ["sprint", "today", "mine"],
"dataSlices": [ "dataSlices": [
"backlog", "backlog",

View File

@ -54,7 +54,7 @@ const ARCHETYPE_UI = {
planDefaultRoute: '/plan/inbox', planDefaultRoute: '/plan/inbox',
workDefaultRoute: '/work/sprint', workDefaultRoute: '/work/sprint',
controlDefaultRoute: '/control/status', controlDefaultRoute: '/control/status',
planOutlineKeys: ['profile', 'inbox', 'sprint', 'gates'], planOutlineKeys: ['profile', 'inbox', 'work', 'sprint', 'gates'],
workNavKeys: ['sprint', 'today', 'mine'], workNavKeys: ['sprint', 'today', 'mine'],
}, },
'initiative.linear_project': { 'initiative.linear_project': {
@ -130,8 +130,10 @@ export function resolveProcessSteps(input = {}) {
export function resolvePlanOutlineNodes(input = {}) { export function resolvePlanOutlineNodes(input = {}) {
const { planOutlineKeys } = resolveMethodUiDefaults(input) const { planOutlineKeys } = resolveMethodUiDefaults(input)
if (!planOutlineKeys) return PLAN_OUTLINE_NODES if (!planOutlineKeys) return PLAN_OUTLINE_NODES
const allowed = new Set(planOutlineKeys) const order = new Map(planOutlineKeys.map((key, index) => [key, index]))
return PLAN_OUTLINE_NODES.filter((node) => allowed.has(node.key)) return PLAN_OUTLINE_NODES.filter((node) => order.has(node.key)).sort(
(a, b) => (order.get(a.key) ?? 0) - (order.get(b.key) ?? 0),
)
} }
/** /**

View File

@ -15,7 +15,7 @@ describe('methodUiDefaults', () => {
}) })
expect(ui.planDefaultRoute).toBe('/plan/inbox') expect(ui.planDefaultRoute).toBe('/plan/inbox')
expect(ui.workDefaultRoute).toBe('/work/sprint') expect(ui.workDefaultRoute).toBe('/work/sprint')
expect(ui.planOutlineKeys).toEqual(['profile', 'inbox', 'sprint', 'gates']) expect(ui.planOutlineKeys).toEqual(['profile', 'inbox', 'work', 'sprint', 'gates'])
expect(resolveWorkNavItems({ archetypeKey: 'initiative.product' }).map((i) => i.key)).toEqual([ expect(resolveWorkNavItems({ archetypeKey: 'initiative.product' }).map((i) => i.key)).toEqual([
'sprint', 'sprint',
'today', 'today',
@ -41,6 +41,12 @@ describe('methodUiDefaults', () => {
]) ])
}) })
it('product plan outline includes work for direct AP path', () => {
expect(
resolvePlanOutlineNodes({ archetypeKey: 'initiative.product' }).map((n) => n.key),
).toEqual(['profile', 'inbox', 'work', 'sprint', 'gates'])
})
it('linear archetype defaults to gates and filters plan outline', () => { it('linear archetype defaults to gates and filters plan outline', () => {
const ui = resolveMethodUiDefaults({ archetypeKey: 'initiative.linear_project' }) const ui = resolveMethodUiDefaults({ archetypeKey: 'initiative.linear_project' })
expect(ui.planDefaultRoute).toBe('/plan/gates') expect(ui.planDefaultRoute).toBe('/plan/gates')

View File

@ -13,6 +13,7 @@ function PlanWorkInner() {
const { initiativeId, projectId: scopeProjectId, hrefWithScope } = useProgramScope() const { initiativeId, projectId: scopeProjectId, hrefWithScope } = useProgramScope()
const [executionGraph, setExecutionGraph] = useState(null) const [executionGraph, setExecutionGraph] = useState(null)
const { const {
initiative,
actions, actions,
projects, projects,
roadmapItems, roadmapItems,
@ -52,6 +53,11 @@ function PlanWorkInner() {
return <LoadingState message="Lade Arbeitspakete …" /> return <LoadingState message="Lade Arbeitspakete …" />
} }
const isProduct = initiative?.archetype_key === 'initiative.product'
const sectionLead = isProduct
? 'Arbeitspakete direkt anlegen (Pfad B) oder committetes Ist — ohne Eingang-Umweg. Sprint-fokussiert unter Ausführen → Sprint.'
: 'Gesamt-Ist am Vorhaben — Sprint-fokussierte Arbeit unter Ausführen → Sprint.'
return ( return (
<> <>
{error && <p className="error">{error}</p>} {error && <p className="error">{error}</p>}
@ -76,7 +82,7 @@ function PlanWorkInner() {
busy={formBusy} busy={formBusy}
executionGraph={executionGraph} executionGraph={executionGraph}
sectionTitle="Alle Arbeitspakete" sectionTitle="Alle Arbeitspakete"
sectionLead="Gesamt-Ist am Vorhaben — Sprint-fokussierte Arbeit unter Ausführen → Sprint." sectionLead={sectionLead}
/> />
</> </>
) )

View File

@ -145,8 +145,10 @@ export function resolveOperatingProfileFromInput(input = {}) {
export function resolvePlanOutlineNodesFromProfile(input = {}) { export function resolvePlanOutlineNodesFromProfile(input = {}) {
const { planOutlineKeys } = resolveOperatingProfileFromInput(input) const { planOutlineKeys } = resolveOperatingProfileFromInput(input)
if (!planOutlineKeys) return PLAN_OUTLINE_NODES if (!planOutlineKeys) return PLAN_OUTLINE_NODES
const allowed = new Set(planOutlineKeys) const order = new Map(planOutlineKeys.map((key, index) => [key, index]))
return PLAN_OUTLINE_NODES.filter((node) => allowed.has(node.key)) return PLAN_OUTLINE_NODES.filter((node) => order.has(node.key)).sort(
(a, b) => (order.get(a.key) ?? 0) - (order.get(b.key) ?? 0),
)
} }
/** /**

View File

@ -21,7 +21,7 @@ const PRODUCT_CONTEXT = {
workDefaultRoute: '/work/sprint', workDefaultRoute: '/work/sprint',
workDefaultRouteWithoutActiveWorkCycle: '/work/today', workDefaultRouteWithoutActiveWorkCycle: '/work/today',
controlDefaultRoute: '/control/status', controlDefaultRoute: '/control/status',
planOutlineKeys: ['profile', 'inbox', 'sprint', 'gates'], planOutlineKeys: ['profile', 'inbox', 'work', 'sprint', 'gates'],
workNavKeys: ['sprint', 'today', 'mine'], workNavKeys: ['sprint', 'today', 'mine'],
}, },
data_slices: ['backlog', 'actions', 'roadmap', 'work_cycles', 'projects'], data_slices: ['backlog', 'actions', 'roadmap', 'work_cycles', 'projects'],