Mitai analysis-split Sub-Nav und weniger Blend-UI bei Auspraegungen.
Some checks failed
Test Suite / lint-backend (push) Waiting to run
Test Suite / compose-smoke (push) Waiting to run
Test Suite / k6 /api/health Baseline (push) Blocked by required conditions
Test Suite / playwright-smoke (push) Blocked by required conditions
Deploy Development / deploy (push) Successful in 46s
Test Suite / pytest-backend (push) Has been cancelled

Modus-Subnav als linke Spalte (Plan/Kontrolle/Ausfuehren), Referenz-Auspraegungen aus Formularen entfernt, Hub-Karten auf Steuerung gestrichen.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Lars 2026-07-10 14:26:14 +02:00
parent b0594df5f2
commit 5e2ae9de31
18 changed files with 293 additions and 440 deletions

View File

@ -8,10 +8,7 @@ import {
getInitiativeFields, getInitiativeFields,
listArchetypeFieldDefinitions, listArchetypeFieldDefinitions,
} from '../api/entityFields.js' } from '../api/entityFields.js'
import { listSteeringMethods } from '../api/steering.js'
import { listMethodProfiles } from '../api/methodProfiles.js'
import { FieldRenderer } from './FieldRenderer.jsx' import { FieldRenderer } from './FieldRenderer.jsx'
import { InitiativeSteeringMeta } from './InitiativeSteeringMeta.jsx'
export function InitiativeForm({ export function InitiativeForm({
initial = {}, initial = {},
@ -26,42 +23,6 @@ export function InitiativeForm({
const [fieldDefinitions, setFieldDefinitions] = useState([]) const [fieldDefinitions, setFieldDefinitions] = useState([])
const [dynamicValues, setDynamicValues] = useState({}) const [dynamicValues, setDynamicValues] = useState({})
const [fieldsLoading, setFieldsLoading] = useState(false) const [fieldsLoading, setFieldsLoading] = useState(false)
const [steeringMethods, setSteeringMethods] = useState([])
const [methodProfiles, setMethodProfiles] = useState([])
const [methodProfileKey, setMethodProfileKey] = useState('')
useEffect(() => {
let cancelled = false
listSteeringMethods()
.then((items) => {
if (!cancelled) setSteeringMethods(Array.isArray(items) ? items : [])
})
.catch(() => {
if (!cancelled) setSteeringMethods([])
})
return () => {
cancelled = true
}
}, [])
useEffect(() => {
let cancelled = false
setMethodProfileKey('')
if (!archetypeKey) {
setMethodProfiles([])
return undefined
}
listMethodProfiles(archetypeKey)
.then((items) => {
if (!cancelled) setMethodProfiles(Array.isArray(items) ? items : [])
})
.catch(() => {
if (!cancelled) setMethodProfiles([])
})
return () => {
cancelled = true
}
}, [archetypeKey])
useEffect(() => { useEffect(() => {
let cancelled = false let cancelled = false
@ -130,7 +91,6 @@ export function InitiativeForm({
status: form.status.value, status: form.status.value,
priority: form.priority.value, priority: form.priority.value,
dynamicFields: dynamicValues, dynamicFields: dynamicValues,
...(methodProfileKey ? { method_profile_key: methodProfileKey } : {}),
}) })
} }
@ -139,6 +99,7 @@ export function InitiativeForm({
} }
const formReady = archetypes && !fieldsLoading const formReady = archetypes && !fieldsLoading
const selectedArchetype = (archetypes || []).find((item) => item.key === archetypeKey)
return ( return (
<form className="form workspace-form" onSubmit={handleSubmit}> <form className="form workspace-form" onSubmit={handleSubmit}>
@ -160,31 +121,10 @@ export function InitiativeForm({
</option> </option>
))} ))}
</select> </select>
</label> {selectedArchetype?.description && (
{methodProfiles.length > 0 && ( <span className="muted form-hint">{selectedArchetype.description}</span>
<label>
Ausprägung (optional)
<select
name="method_profile_key"
value={methodProfileKey}
onChange={(e) => setMethodProfileKey(e.target.value)}
>
<option value=""> Standard (nur Archetyp) </option>
{methodProfiles.map((item) => (
<option key={item.key} value={item.key}>
{item.label}
</option>
))}
</select>
</label>
)} )}
<InitiativeSteeringMeta </label>
archetypeKey={archetypeKey}
initiativeArchetypes={archetypes || []}
steeringMethods={steeringMethods}
methodProfiles={methodProfiles}
selectedMethodProfileKey={methodProfileKey}
/>
<label> <label>
Kurzbeschreibung (Ziel) Kurzbeschreibung (Ziel)
<textarea name="goal" rows={2} defaultValue={initial.goal || ''} /> <textarea name="goal" rows={2} defaultValue={initial.goal || ''} />

View File

@ -1,60 +0,0 @@
import { Link } from 'react-router-dom'
import { scopedPath } from '../utils/routes.js'
const AREAS = [
{
to: '/plan/gates',
title: 'Plan',
description: 'Roadmap, Gates und Plan-Ist — methodenneutral pflegen.',
countKey: 'roadmapItems',
},
{
to: '/work/today',
title: 'Ausführung',
description: 'Arbeitspakete, Zuweisungen, Blocker am Objekt.',
countKey: 'actionsOpen',
},
{
to: '/plan/inbox',
title: 'Eingang',
description: 'Backlog triagieren und in Arbeitspakete überführen.',
countKey: 'backlog',
},
{
to: '/control/journey',
title: 'Nachvollziehbarkeit',
description: 'Entscheidungen, Reviews, Nachweise über die Zeit.',
countKey: null,
},
]
export function InitiativeNavCards({ initiativeId, counts = {} }) {
return (
<section className="initiative-nav-cards">
<h2 className="initiative-nav-cards-title">Pflege & Planung</h2>
<div className="initiative-nav-cards-grid">
{AREAS.map((area) => {
const hint =
area.countKey === 'backlog' && counts.backlog
? `${counts.backlog} Backlog`
: area.countKey === 'actionsOpen' && counts.actionsOpen
? `${counts.actionsOpen} offen`
: area.countKey === 'roadmapItems' && counts.roadmapItems
? `${counts.roadmapItems} Plan-Elemente`
: null
return (
<Link
key={area.to}
to={scopedPath(area.to, { initiativeId })}
className="initiative-nav-card card"
>
<h3>{area.title}</h3>
<p className="muted">{area.description}</p>
{hint && <span className="initiative-nav-card-hint">{hint}</span>}
</Link>
)
})}
</div>
</section>
)
}

View File

@ -3,38 +3,21 @@ import {
findArchetype, findArchetype,
methodDescriptionForKey, methodDescriptionForKey,
methodLabelForKey, methodLabelForKey,
methodProfileDescription,
methodProfileLabel,
} from '../utils/archetypes.js' } from '../utils/archetypes.js'
/** /**
* Zeigt Archetyp Methode Ausprägung transparent (Capture & Lagebild). * Archetyp und abgeleitete Steuerungsmethode (read-only Kontext).
*/ */
export function InitiativeSteeringMeta({ export function InitiativeSteeringMeta({
archetypeKey, archetypeKey,
initiativeArchetypes = [], initiativeArchetypes = [],
steeringSnapshot = null, steeringSnapshot = null,
steeringMethods = [], steeringMethods = [],
methodProfiles = [],
selectedMethodProfileKey = '',
compact = false, compact = false,
}) { }) {
const archetype = findArchetype(archetypeKey, initiativeArchetypes) const archetype = findArchetype(archetypeKey, initiativeArchetypes)
const profileKey =
steeringSnapshot?.method_profile_key || selectedMethodProfileKey || null
const profileLabel =
steeringSnapshot?.method_profile_label ||
methodProfileLabel(profileKey, methodProfiles)
const profileDescription = methodProfileDescription(profileKey, methodProfiles)
const selectedProfile = profileKey
? methodProfiles.find((item) => item.key === profileKey)
: null
const methodKey = const methodKey =
steeringSnapshot?.method_key || steeringSnapshot?.method_key || archetype?.default_method_key || null
selectedProfile?.method_key ||
archetype?.default_method_key ||
null
const methodLabel = const methodLabel =
steeringSnapshot?.method_label || methodLabelForKey(methodKey, steeringMethods) steeringSnapshot?.method_label || methodLabelForKey(methodKey, steeringMethods)
const methodDescription = methodDescriptionForKey(methodKey, steeringMethods) const methodDescription = methodDescriptionForKey(methodKey, steeringMethods)
@ -62,19 +45,6 @@ export function InitiativeSteeringMeta({
</span> </span>
</div> </div>
)} )}
{profileKey && (
<div className="initiative-steering-meta__row">
<span className="initiative-steering-meta__label">Ausprägung</span>
<span className="initiative-steering-meta__value">
<strong>{profileLabel}</strong>
{!compact && profileDescription && (
<span className="initiative-steering-meta__hint muted">
{profileDescription}
</span>
)}
</span>
</div>
)}
{methodKey && ( {methodKey && (
<div className="initiative-steering-meta__row"> <div className="initiative-steering-meta__row">
<span className="initiative-steering-meta__label">Steuerungsmethode</span> <span className="initiative-steering-meta__label">Steuerungsmethode</span>
@ -85,7 +55,7 @@ export function InitiativeSteeringMeta({
{methodDescription} {methodDescription}
</span> </span>
)} )}
{!compact && !steeringSnapshot && archetype?.default_method_key && !profileKey && ( {!compact && !steeringSnapshot && archetype?.default_method_key && (
<span className="initiative-steering-meta__hint muted"> <span className="initiative-steering-meta__hint muted">
Wird beim Anlegen automatisch vom Archetyp abgeleitet. Wird beim Anlegen automatisch vom Archetyp abgeleitet.
</span> </span>

View File

@ -1,56 +0,0 @@
import { useEffect, useState } from 'react'
import { listMethodProfiles } from '../api/methodProfiles.js'
export function MethodProfileSelect({
archetypeKey,
value = '',
onChange,
disabled = false,
}) {
const [profiles, setProfiles] = useState([])
useEffect(() => {
if (!archetypeKey) {
setProfiles([])
return undefined
}
let cancelled = false
listMethodProfiles(archetypeKey)
.then((items) => {
if (!cancelled) setProfiles(Array.isArray(items) ? items : [])
})
.catch(() => {
if (!cancelled) setProfiles([])
})
return () => {
cancelled = true
}
}, [archetypeKey])
if (profiles.length === 0) {
return (
<p className="muted method-profile-select__empty">
Keine Referenz-Ausprägung für diesen Archetyp hinterlegt.
</p>
)
}
return (
<label className="method-profile-select">
Ausprägung
<select
value={value || ''}
disabled={disabled}
onChange={(e) => onChange?.(e.target.value)}
aria-label="Methoden-Ausprägung"
>
<option value=""> Standard (nur Archetyp) </option>
{profiles.map((item) => (
<option key={item.key} value={item.key}>
{item.label}
</option>
))}
</select>
</label>
)
}

View File

@ -0,0 +1,63 @@
import { NavLink, useLocation } from 'react-router-dom'
import { useProgramScope } from '../context/ProgramScopeContext.jsx'
/**
* Bereichs-Subnav (Mitai Settings/Admin-Shell).
*
* @param {{
* modeLabel: string,
* items: { key: string, to: string, label: string, labelFor?: (ctx: object) => string }[],
* resolveActiveKey?: (pathname: string) => string | null,
* showScope?: boolean,
* scopeMutedLabel?: string,
* }} props
*/
export function ModeAreaNav({
modeLabel,
items,
resolveActiveKey,
showScope = false,
scopeMutedLabel = 'Portfolio',
}) {
const location = useLocation()
const { initiativeId, initiativeTitle, hrefWithScope } = useProgramScope()
const activeKey = resolveActiveKey?.(location.pathname)
return (
<nav className="analysis-split__nav" aria-label={`${modeLabel}-Navigation`}>
<div className="analysis-split__nav-header">
<span className="analysis-split__nav-eyebrow">{modeLabel}</span>
{showScope &&
(initiativeId ? (
<p className="analysis-split__nav-scope" title={initiativeTitle || initiativeId}>
{initiativeTitle || 'Vorhaben'}
</p>
) : (
<p className="analysis-split__nav-scope analysis-split__nav-scope--muted">
{scopeMutedLabel}
</p>
))}
</div>
<ul className="analysis-split__nav-list">
{items.map((item) => {
const label = item.labelFor ? item.labelFor({}) : item.label
const isActive = activeKey === item.key
return (
<li key={item.key}>
<NavLink
to={hrefWithScope(item.to)}
end
className={({ isActive: routerActive }) =>
'analysis-split__nav-link' +
(routerActive || isActive ? ' analysis-split__nav-link--active' : '')
}
>
{label}
</NavLink>
</li>
)
})}
</ul>
</nav>
)
}

View File

@ -0,0 +1,11 @@
/**
* Mitai analysis-split: linke Sub-Nav + Hauptinhalt (Desktop), gestapelt (Mobile).
*/
export function ModeAreaShell({ nav, children }) {
return (
<div className="analysis-split">
{nav}
<div className="analysis-split__main">{children}</div>
</div>
)
}

View File

@ -1,26 +0,0 @@
import { NavLink } from 'react-router-dom'
import { useProgramScope } from '../context/ProgramScopeContext.jsx'
/**
* @param {{ items: { to: string, label: string, end?: boolean }[] }} props
*/
export function ModeSubNav({ items }) {
const { hrefWithScope } = useProgramScope()
return (
<nav className="mode-subnav" aria-label="Modus-Unternavigation">
{items.map((item) => (
<NavLink
key={item.to}
to={hrefWithScope(item.to)}
end={item.end !== false}
className={({ isActive }) =>
'mode-subnav__link' + (isActive ? ' mode-subnav__link--active' : '')
}
>
{item.label}
</NavLink>
))}
</nav>
)
}

View File

@ -1,10 +1,8 @@
import { NavLink, useLocation } from 'react-router-dom' import { NavLink, useLocation } from 'react-router-dom'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { getInitiative, listInitiativeActions } from '../api/initiatives.js' import { listInitiativeActions } from '../api/initiatives.js'
import { listInitiativeBacklog } from '../api/backlog.js' import { listInitiativeBacklog } from '../api/backlog.js'
import { useProgramScope } from '../context/ProgramScopeContext.jsx' import { useProgramScope } from '../context/ProgramScopeContext.jsx'
import { useEntityArchetypes } from '../hooks/useEntityArchetypes.js'
import { InitiativeSteeringMeta } from './InitiativeSteeringMeta.jsx'
import { import {
PLAN_OUTLINE_NODES, PLAN_OUTLINE_NODES,
resolvePlanOutlineActiveKey, resolvePlanOutlineActiveKey,
@ -13,28 +11,8 @@ import {
export function PlanOutlineNav() { export function PlanOutlineNav() {
const location = useLocation() const location = useLocation()
const { initiativeId, initiativeTitle, hrefWithScope } = useProgramScope() const { initiativeId, initiativeTitle, hrefWithScope } = useProgramScope()
const { initiativeArchetypes } = useEntityArchetypes()
const activeKey = resolvePlanOutlineActiveKey(location.pathname) const activeKey = resolvePlanOutlineActiveKey(location.pathname)
const [counts, setCounts] = useState({ inbox: null, work: null }) const [counts, setCounts] = useState({ inbox: null, work: null })
const [initiativeArchetypeKey, setInitiativeArchetypeKey] = useState('')
useEffect(() => {
if (!initiativeId) {
setInitiativeArchetypeKey('')
return undefined
}
let cancelled = false
getInitiative(initiativeId)
.then((data) => {
if (!cancelled) setInitiativeArchetypeKey(data.archetype_key || '')
})
.catch(() => {
if (!cancelled) setInitiativeArchetypeKey('')
})
return () => {
cancelled = true
}
}, [initiativeId])
useEffect(() => { useEffect(() => {
if (!initiativeId) { if (!initiativeId) {
@ -70,29 +48,21 @@ export function PlanOutlineNav() {
} }
return ( return (
<nav className="plan-outline-nav" aria-label="Plan-Outline"> <nav className="analysis-split__nav" aria-label="Plan-Navigation">
<div className="plan-outline-nav__header"> <div className="analysis-split__nav-header">
<span className="plan-outline-nav__eyebrow">Planen</span> <span className="analysis-split__nav-eyebrow">Planen</span>
{initiativeId ? ( {initiativeId ? (
<p className="plan-outline-nav__root" title={initiativeTitle || initiativeId}> <p className="analysis-split__nav-scope" title={initiativeTitle || initiativeId}>
{initiativeTitle || 'Vorhaben'} {initiativeTitle || 'Vorhaben'}
</p> </p>
) : ( ) : (
<p className="plan-outline-nav__root plan-outline-nav__root--muted"> <p className="analysis-split__nav-scope analysis-split__nav-scope--muted">
Portfolio Vorhaben wählen Portfolio Vorhaben wählen
</p> </p>
)} )}
</div> </div>
{initiativeId && initiativeArchetypeKey && ( <ul className="analysis-split__nav-list">
<InitiativeSteeringMeta
archetypeKey={initiativeArchetypeKey}
initiativeArchetypes={initiativeArchetypes}
compact
/>
)}
<ul className="plan-outline-nav__list">
{PLAN_OUTLINE_NODES.map((node) => { {PLAN_OUTLINE_NODES.map((node) => {
const needsInitiative = node.requiresInitiative && !initiativeId const needsInitiative = node.requiresInitiative && !initiativeId
const isDisabled = node.disabled || needsInitiative const isDisabled = node.disabled || needsInitiative
@ -102,8 +72,8 @@ export function PlanOutlineNav() {
<li key={node.key}> <li key={node.key}>
<span <span
className={ className={
'plan-outline-nav__link plan-outline-nav__link--disabled' + 'analysis-split__nav-link analysis-split__nav-link--disabled' +
(activeKey === node.key ? ' plan-outline-nav__link--active' : '') (activeKey === node.key ? ' analysis-split__nav-link--active' : '')
} }
aria-disabled="true" aria-disabled="true"
title={needsInitiative ? 'Vorhaben im Scope wählen' : node.hint} title={needsInitiative ? 'Vorhaben im Scope wählen' : node.hint}
@ -120,8 +90,8 @@ export function PlanOutlineNav() {
to={hrefWithScope(node.to)} to={hrefWithScope(node.to)}
end end
className={({ isActive }) => className={({ isActive }) =>
'plan-outline-nav__link' + 'analysis-split__nav-link' +
(isActive ? ' plan-outline-nav__link--active' : '') (isActive ? ' analysis-split__nav-link--active' : '')
} }
> >
{nodeLabel(node)} {nodeLabel(node)}

View File

@ -54,8 +54,6 @@ export function SteeringSnapshotPanel({
method_label, method_label,
archetype_key, archetype_key,
archetype_label, archetype_label,
method_profile_key,
method_profile_label,
steering_guidance, steering_guidance,
attention_items = [], attention_items = [],
signals = [], signals = [],
@ -117,13 +115,6 @@ export function SteeringSnapshotPanel({
{(archetype_label || archetype_key) && ( {(archetype_label || archetype_key) && (
<p className="steering-archetype-row muted"> <p className="steering-archetype-row muted">
Archetyp: <strong>{archetype_label || archetype_key}</strong> Archetyp: <strong>{archetype_label || archetype_key}</strong>
{method_profile_label || method_profile_key ? (
<>
{' '}
· Ausprägung:{' '}
<strong>{method_profile_label || method_profile_key}</strong>
</>
) : null}
</p> </p>
)} )}
{(method_label || canManageMethod) && ( {(method_label || canManageMethod) && (

View File

@ -0,0 +1,24 @@
/**
* Kontrolle-Subnav (Mitai: settingsNav / analysis-split).
* Pfade müssen mit Routes in App.jsx übereinstimmen.
*
* @typedef {{ key: string, to: string, label: string }} ModeNavItem
*/
/** @type {ModeNavItem[]} */
export const CONTROL_NAV_ITEMS = [
{ key: 'status', to: '/control/status', label: 'Steuerung' },
{ key: 'journey', to: '/control/journey', label: 'Nachvollziehbarkeit' },
]
/**
* @param {string} pathname
* @returns {string | null}
*/
export function resolveControlNavActiveKey(pathname) {
const path = pathname.replace(/\/$/, '') || pathname
const match = CONTROL_NAV_ITEMS.find(
(item) => path === item.to || path.startsWith(`${item.to}/`),
)
return match?.key ?? null
}

View File

@ -0,0 +1,18 @@
import { describe, expect, it } from 'vitest'
import { resolveControlNavActiveKey } from '../config/controlNav.js'
import { resolveWorkNavActiveKey } from '../config/workNav.js'
describe('controlNav', () => {
it('resolves active control sub-route', () => {
expect(resolveControlNavActiveKey('/control/status')).toBe('status')
expect(resolveControlNavActiveKey('/control/journey')).toBe('journey')
expect(resolveControlNavActiveKey('/control')).toBe(null)
})
})
describe('workNav', () => {
it('resolves active work sub-route', () => {
expect(resolveWorkNavActiveKey('/work/today')).toBe('today')
expect(resolveWorkNavActiveKey('/work/mine')).toBe('mine')
})
})

View File

@ -0,0 +1,24 @@
/**
* Ausführen-Subnav (Mitai: settingsNav / analysis-split).
* Pfade müssen mit Routes in App.jsx übereinstimmen.
*
* @typedef {{ key: string, to: string, label: string }} ModeNavItem
*/
/** @type {ModeNavItem[]} */
export const WORK_NAV_ITEMS = [
{ key: 'today', to: '/work/today', label: 'Heute' },
{ key: 'mine', to: '/work/mine', label: 'Meine Queue' },
]
/**
* @param {string} pathname
* @returns {string | null}
*/
export function resolveWorkNavActiveKey(pathname) {
const path = pathname.replace(/\/$/, '') || pathname
const match = WORK_NAV_ITEMS.find(
(item) => path === item.to || path.startsWith(`${item.to}/`),
)
return match?.key ?? null
}

View File

@ -3,7 +3,6 @@ import { scopedPath } from '../../utils/routes.js'
import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx' import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx'
import { SteeringSnapshotPanel } from '../../components/SteeringSnapshotPanel.jsx' import { SteeringSnapshotPanel } from '../../components/SteeringSnapshotPanel.jsx'
import { NextActionWidget } from '../../widgets/NextActionWidget.jsx' import { NextActionWidget } from '../../widgets/NextActionWidget.jsx'
import { InitiativeNavCards } from '../../components/InitiativeNavCards.jsx'
export function InitiativeOverviewPage() { export function InitiativeOverviewPage() {
const { const {
@ -16,9 +15,6 @@ export function InitiativeOverviewPage() {
methodBusy, methodBusy,
capabilities, capabilities,
handleMethodChange, handleMethodChange,
backlogItems,
roadmapItems,
milestones,
} = useInitiativeOperations() } = useInitiativeOperations()
const counts = steeringSnapshot?.counts || {} const counts = steeringSnapshot?.counts || {}
@ -66,15 +62,6 @@ export function InitiativeOverviewPage() {
</Link> </Link>
</section> </section>
)} )}
<InitiativeNavCards
initiativeId={initiativeId}
counts={{
backlog: backlogItems.length,
roadmapItems: roadmapItems.length,
actionsOpen: counts.actions_open,
}}
/>
</> </>
) )
} }

View File

@ -1,18 +1,29 @@
import { Navigate, Outlet } from 'react-router-dom' import { Navigate, Outlet } from 'react-router-dom'
import { ModeAreaShell } from '../../components/ModeAreaShell.jsx'
import { ModeAreaNav } from '../../components/ModeAreaNav.jsx'
import { ModeShell } from '../../components/ModeShell.jsx' import { ModeShell } from '../../components/ModeShell.jsx'
import { ModeSubNav } from '../../components/ModeSubNav.jsx' import {
CONTROL_NAV_ITEMS,
const CONTROL_NAV = [ resolveControlNavActiveKey,
{ to: '/control/status', label: 'Steuerung', end: true }, } from '../../config/controlNav.js'
{ to: '/control/journey', label: 'Journey', end: true },
]
export function ControlLayout() { export function ControlLayout() {
return ( return (
<div className="app-page"> <div className="app-page">
<ModeShell modeKey="control" /> <ModeShell modeKey="control" />
<ModeSubNav items={CONTROL_NAV} /> <ModeAreaShell
nav={
<ModeAreaNav
modeLabel="Kontrolle"
items={CONTROL_NAV_ITEMS}
resolveActiveKey={resolveControlNavActiveKey}
showScope
scopeMutedLabel="Vorhaben wählen"
/>
}
>
<Outlet /> <Outlet />
</ModeAreaShell>
</div> </div>
) )
} }

View File

@ -1,17 +1,15 @@
import { Outlet } from 'react-router-dom' import { Outlet } from 'react-router-dom'
import { ModeAreaShell } from '../../components/ModeAreaShell.jsx'
import { ModeShell } from '../../components/ModeShell.jsx' import { ModeShell } from '../../components/ModeShell.jsx'
import { PlanOutlineNav } from '../../components/PlanOutlineNav.jsx' import { PlanOutlineNav } from '../../components/PlanOutlineNav.jsx'
export function PlanLayout() { export function PlanLayout() {
return ( return (
<div className="app-page plan-outline-shell"> <div className="app-page">
<ModeShell modeKey="plan" /> <ModeShell modeKey="plan" />
<div className="plan-outline-shell__body"> <ModeAreaShell nav={<PlanOutlineNav />}>
<PlanOutlineNav />
<div className="plan-outline-shell__content">
<Outlet /> <Outlet />
</div> </ModeAreaShell>
</div>
</div> </div>
) )
} }

View File

@ -7,7 +7,6 @@ import { LoadingState } from '../../components/LoadingState.jsx'
import { Modal } from '../../components/Modal.jsx' import { Modal } from '../../components/Modal.jsx'
import { InitiativeForm } from '../../components/InitiativeForm.jsx' import { InitiativeForm } from '../../components/InitiativeForm.jsx'
import { InitiativeSteeringMeta } from '../../components/InitiativeSteeringMeta.jsx' import { InitiativeSteeringMeta } from '../../components/InitiativeSteeringMeta.jsx'
import { MethodProfileSelect } from '../../components/MethodProfileSelect.jsx'
import { PriorityBadge } from '../../components/PriorityBadge.jsx' import { PriorityBadge } from '../../components/PriorityBadge.jsx'
import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx' import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx'
import { useProgramScope } from '../../context/ProgramScopeContext.jsx' import { useProgramScope } from '../../context/ProgramScopeContext.jsx'
@ -88,8 +87,6 @@ function PlanProfileInner() {
formBusy, formBusy,
steeringSnapshot, steeringSnapshot,
steeringMethods, steeringMethods,
methodBusy,
handleMethodProfileChange,
actors, actors,
projects, projects,
} = ops } = ops
@ -143,16 +140,6 @@ function PlanProfileInner() {
steeringSnapshot={steeringSnapshot} steeringSnapshot={steeringSnapshot}
steeringMethods={steeringMethods} steeringMethods={steeringMethods}
/> />
{canManage && (
<div className="plan-profile__steering-edit">
<MethodProfileSelect
archetypeKey={initiative.archetype_key}
value={steeringSnapshot?.method_profile_key || ''}
disabled={methodBusy}
onChange={handleMethodProfileChange}
/>
</div>
)}
<div className="plan-profile__field"> <div className="plan-profile__field">
<h3 className="plan-profile__label">Verantwortlich (Owner)</h3> <h3 className="plan-profile__label">Verantwortlich (Owner)</h3>
<p>{ownerName}</p> <p>{ownerName}</p>

View File

@ -1,18 +1,24 @@
import { Navigate, Outlet } from 'react-router-dom' import { Navigate, Outlet } from 'react-router-dom'
import { ModeAreaShell } from '../../components/ModeAreaShell.jsx'
import { ModeAreaNav } from '../../components/ModeAreaNav.jsx'
import { ModeShell } from '../../components/ModeShell.jsx' import { ModeShell } from '../../components/ModeShell.jsx'
import { ModeSubNav } from '../../components/ModeSubNav.jsx' import { WORK_NAV_ITEMS, resolveWorkNavActiveKey } from '../../config/workNav.js'
const WORK_NAV = [
{ to: '/work/today', label: 'Heute', end: true },
{ to: '/work/mine', label: 'Meine Queue', end: true },
]
export function WorkLayout() { export function WorkLayout() {
return ( return (
<div className="app-page"> <div className="app-page">
<ModeShell modeKey="work" /> <ModeShell modeKey="work" />
<ModeSubNav items={WORK_NAV} /> <ModeAreaShell
nav={
<ModeAreaNav
modeLabel="Ausführen"
items={WORK_NAV_ITEMS}
resolveActiveKey={resolveWorkNavActiveKey}
/>
}
>
<Outlet /> <Outlet />
</ModeAreaShell>
</div> </div>
) )
} }

View File

@ -223,28 +223,110 @@
white-space: nowrap; white-space: nowrap;
} }
.mode-subnav { /* Mitai analysis-split: Bereichs-Shell mit linker Sub-Nav (Desktop) */
display: flex; .analysis-split {
flex-wrap: wrap; display: grid;
gap: 8px; grid-template-columns: minmax(200px, 240px) minmax(0, 1fr);
margin-bottom: 16px; gap: 20px;
align-items: start;
} }
.mode-subnav__link { .analysis-split__main {
padding: 8px 14px; min-width: 0;
border-radius: 999px; }
text-decoration: none;
font-size: 13px; .analysis-split__nav {
font-weight: 600; position: sticky;
color: var(--jk-text-muted); top: 12px;
padding: 14px;
border-radius: 12px;
border: 1px solid var(--jk-border); border: 1px solid var(--jk-border);
background: var(--jk-surface); background: var(--jk-surface);
} }
.mode-subnav__link--active { .analysis-split__nav-header {
color: var(--jk-primary); margin-bottom: 12px;
border-color: var(--jk-primary); padding-bottom: 10px;
border-bottom: 1px solid var(--jk-border);
}
.analysis-split__nav-eyebrow {
display: block;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--jk-text-muted);
}
.analysis-split__nav-scope {
margin: 6px 0 0;
font-size: 15px;
font-weight: 700;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
}
.analysis-split__nav-scope--muted {
font-weight: 600;
color: var(--jk-text-muted);
}
.analysis-split__nav-list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 4px;
}
.analysis-split__nav-link {
display: block;
padding: 8px 10px;
border-radius: 8px;
text-decoration: none;
font-size: 13px;
font-weight: 600;
color: var(--jk-text-muted);
}
.analysis-split__nav-link:hover {
background: var(--jk-primary-soft); background: var(--jk-primary-soft);
color: var(--jk-text);
}
.analysis-split__nav-link--active {
color: var(--jk-primary);
background: var(--jk-primary-soft);
}
.analysis-split__nav-link--disabled {
opacity: 0.55;
cursor: not-allowed;
}
@media (max-width: 900px) {
.analysis-split {
grid-template-columns: 1fr;
}
.analysis-split__nav-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.analysis-split__nav-link {
padding: 8px 14px;
border: 1px solid var(--jk-border);
border-radius: 999px;
background: var(--jk-surface);
}
.analysis-split__nav-link--active {
border-color: var(--jk-primary);
}
} }
.scope-picker__list { .scope-picker__list {
@ -292,93 +374,6 @@
margin-bottom: 12px; margin-bottom: 12px;
} }
.plan-outline-shell__body {
display: grid;
grid-template-columns: minmax(200px, 240px) minmax(0, 1fr);
gap: 20px;
align-items: start;
}
@media (max-width: 900px) {
.plan-outline-shell__body {
grid-template-columns: 1fr;
}
}
.plan-outline-nav {
position: sticky;
top: 12px;
padding: 14px;
border-radius: 12px;
border: 1px solid var(--jk-border);
background: var(--jk-surface);
}
.plan-outline-nav__header {
margin-bottom: 12px;
padding-bottom: 10px;
border-bottom: 1px solid var(--jk-border);
}
.plan-outline-nav__eyebrow {
display: block;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--jk-text-muted);
}
.plan-outline-nav__root {
margin: 6px 0 0;
font-size: 15px;
font-weight: 700;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
}
.plan-outline-nav__root--muted {
font-weight: 600;
color: var(--jk-text-muted);
}
.plan-outline-nav__list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 4px;
}
.plan-outline-nav__link {
display: block;
padding: 8px 10px;
border-radius: 8px;
text-decoration: none;
font-size: 13px;
font-weight: 600;
color: var(--jk-text-muted);
}
.plan-outline-nav__link:hover {
background: var(--jk-primary-soft);
color: var(--jk-text);
}
.plan-outline-nav__link--active {
color: var(--jk-primary);
background: var(--jk-primary-soft);
}
.plan-outline-nav__link--disabled {
opacity: 0.55;
cursor: not-allowed;
}
.plan-outline-shell__content {
min-width: 0;
}
.plan-profile__header { .plan-profile__header {
display: flex; display: flex;