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",
"workDefaultRouteWithoutActiveWorkCycle": "/work/today",
"controlDefaultRoute": "/control/status",
"planOutlineKeys": ["profile", "inbox", "sprint", "gates"],
"planOutlineKeys": ["profile", "inbox", "work", "sprint", "gates"],
"workNavKeys": ["sprint", "today", "mine"],
"dataSlices": [
"backlog",

View File

@ -54,7 +54,7 @@ const ARCHETYPE_UI = {
planDefaultRoute: '/plan/inbox',
workDefaultRoute: '/work/sprint',
controlDefaultRoute: '/control/status',
planOutlineKeys: ['profile', 'inbox', 'sprint', 'gates'],
planOutlineKeys: ['profile', 'inbox', 'work', 'sprint', 'gates'],
workNavKeys: ['sprint', 'today', 'mine'],
},
'initiative.linear_project': {
@ -130,8 +130,10 @@ export function resolveProcessSteps(input = {}) {
export function resolvePlanOutlineNodes(input = {}) {
const { planOutlineKeys } = resolveMethodUiDefaults(input)
if (!planOutlineKeys) return PLAN_OUTLINE_NODES
const allowed = new Set(planOutlineKeys)
return PLAN_OUTLINE_NODES.filter((node) => allowed.has(node.key))
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),
)
}
/**

View File

@ -15,7 +15,7 @@ describe('methodUiDefaults', () => {
})
expect(ui.planDefaultRoute).toBe('/plan/inbox')
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([
'sprint',
'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', () => {
const ui = resolveMethodUiDefaults({ archetypeKey: 'initiative.linear_project' })
expect(ui.planDefaultRoute).toBe('/plan/gates')

View File

@ -13,6 +13,7 @@ function PlanWorkInner() {
const { initiativeId, projectId: scopeProjectId, hrefWithScope } = useProgramScope()
const [executionGraph, setExecutionGraph] = useState(null)
const {
initiative,
actions,
projects,
roadmapItems,
@ -52,6 +53,11 @@ function PlanWorkInner() {
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 (
<>
{error && <p className="error">{error}</p>}
@ -76,7 +82,7 @@ function PlanWorkInner() {
busy={formBusy}
executionGraph={executionGraph}
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 = {}) {
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))
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),
)
}
/**

View File

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