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
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:
parent
b0594df5f2
commit
5e2ae9de31
|
|
@ -8,10 +8,7 @@ import {
|
|||
getInitiativeFields,
|
||||
listArchetypeFieldDefinitions,
|
||||
} from '../api/entityFields.js'
|
||||
import { listSteeringMethods } from '../api/steering.js'
|
||||
import { listMethodProfiles } from '../api/methodProfiles.js'
|
||||
import { FieldRenderer } from './FieldRenderer.jsx'
|
||||
import { InitiativeSteeringMeta } from './InitiativeSteeringMeta.jsx'
|
||||
|
||||
export function InitiativeForm({
|
||||
initial = {},
|
||||
|
|
@ -26,42 +23,6 @@ export function InitiativeForm({
|
|||
const [fieldDefinitions, setFieldDefinitions] = useState([])
|
||||
const [dynamicValues, setDynamicValues] = useState({})
|
||||
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(() => {
|
||||
let cancelled = false
|
||||
|
|
@ -130,7 +91,6 @@ export function InitiativeForm({
|
|||
status: form.status.value,
|
||||
priority: form.priority.value,
|
||||
dynamicFields: dynamicValues,
|
||||
...(methodProfileKey ? { method_profile_key: methodProfileKey } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -139,6 +99,7 @@ export function InitiativeForm({
|
|||
}
|
||||
|
||||
const formReady = archetypes && !fieldsLoading
|
||||
const selectedArchetype = (archetypes || []).find((item) => item.key === archetypeKey)
|
||||
|
||||
return (
|
||||
<form className="form workspace-form" onSubmit={handleSubmit}>
|
||||
|
|
@ -160,31 +121,10 @@ export function InitiativeForm({
|
|||
</option>
|
||||
))}
|
||||
</select>
|
||||
{selectedArchetype?.description && (
|
||||
<span className="muted form-hint">{selectedArchetype.description}</span>
|
||||
)}
|
||||
</label>
|
||||
{methodProfiles.length > 0 && (
|
||||
<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
|
||||
archetypeKey={archetypeKey}
|
||||
initiativeArchetypes={archetypes || []}
|
||||
steeringMethods={steeringMethods}
|
||||
methodProfiles={methodProfiles}
|
||||
selectedMethodProfileKey={methodProfileKey}
|
||||
/>
|
||||
<label>
|
||||
Kurzbeschreibung (Ziel)
|
||||
<textarea name="goal" rows={2} defaultValue={initial.goal || ''} />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -3,38 +3,21 @@ import {
|
|||
findArchetype,
|
||||
methodDescriptionForKey,
|
||||
methodLabelForKey,
|
||||
methodProfileDescription,
|
||||
methodProfileLabel,
|
||||
} from '../utils/archetypes.js'
|
||||
|
||||
/**
|
||||
* Zeigt Archetyp → Methode → Ausprägung transparent (Capture & Lagebild).
|
||||
* Archetyp und abgeleitete Steuerungsmethode (read-only Kontext).
|
||||
*/
|
||||
export function InitiativeSteeringMeta({
|
||||
archetypeKey,
|
||||
initiativeArchetypes = [],
|
||||
steeringSnapshot = null,
|
||||
steeringMethods = [],
|
||||
methodProfiles = [],
|
||||
selectedMethodProfileKey = '',
|
||||
compact = false,
|
||||
}) {
|
||||
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 =
|
||||
steeringSnapshot?.method_key ||
|
||||
selectedProfile?.method_key ||
|
||||
archetype?.default_method_key ||
|
||||
null
|
||||
steeringSnapshot?.method_key || archetype?.default_method_key || null
|
||||
const methodLabel =
|
||||
steeringSnapshot?.method_label || methodLabelForKey(methodKey, steeringMethods)
|
||||
const methodDescription = methodDescriptionForKey(methodKey, steeringMethods)
|
||||
|
|
@ -62,19 +45,6 @@ export function InitiativeSteeringMeta({
|
|||
</span>
|
||||
</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 && (
|
||||
<div className="initiative-steering-meta__row">
|
||||
<span className="initiative-steering-meta__label">Steuerungsmethode</span>
|
||||
|
|
@ -85,7 +55,7 @@ export function InitiativeSteeringMeta({
|
|||
{methodDescription}
|
||||
</span>
|
||||
)}
|
||||
{!compact && !steeringSnapshot && archetype?.default_method_key && !profileKey && (
|
||||
{!compact && !steeringSnapshot && archetype?.default_method_key && (
|
||||
<span className="initiative-steering-meta__hint muted">
|
||||
Wird beim Anlegen automatisch vom Archetyp abgeleitet.
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
63
frontend/src/components/ModeAreaNav.jsx
Normal file
63
frontend/src/components/ModeAreaNav.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
11
frontend/src/components/ModeAreaShell.jsx
Normal file
11
frontend/src/components/ModeAreaShell.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,10 +1,8 @@
|
|||
import { NavLink, useLocation } from 'react-router-dom'
|
||||
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 { useProgramScope } from '../context/ProgramScopeContext.jsx'
|
||||
import { useEntityArchetypes } from '../hooks/useEntityArchetypes.js'
|
||||
import { InitiativeSteeringMeta } from './InitiativeSteeringMeta.jsx'
|
||||
import {
|
||||
PLAN_OUTLINE_NODES,
|
||||
resolvePlanOutlineActiveKey,
|
||||
|
|
@ -13,28 +11,8 @@ import {
|
|||
export function PlanOutlineNav() {
|
||||
const location = useLocation()
|
||||
const { initiativeId, initiativeTitle, hrefWithScope } = useProgramScope()
|
||||
const { initiativeArchetypes } = useEntityArchetypes()
|
||||
const activeKey = resolvePlanOutlineActiveKey(location.pathname)
|
||||
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(() => {
|
||||
if (!initiativeId) {
|
||||
|
|
@ -70,29 +48,21 @@ export function PlanOutlineNav() {
|
|||
}
|
||||
|
||||
return (
|
||||
<nav className="plan-outline-nav" aria-label="Plan-Outline">
|
||||
<div className="plan-outline-nav__header">
|
||||
<span className="plan-outline-nav__eyebrow">Planen</span>
|
||||
<nav className="analysis-split__nav" aria-label="Plan-Navigation">
|
||||
<div className="analysis-split__nav-header">
|
||||
<span className="analysis-split__nav-eyebrow">Planen</span>
|
||||
{initiativeId ? (
|
||||
<p className="plan-outline-nav__root" title={initiativeTitle || initiativeId}>
|
||||
<p className="analysis-split__nav-scope" title={initiativeTitle || initiativeId}>
|
||||
{initiativeTitle || 'Vorhaben'}
|
||||
</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
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{initiativeId && initiativeArchetypeKey && (
|
||||
<InitiativeSteeringMeta
|
||||
archetypeKey={initiativeArchetypeKey}
|
||||
initiativeArchetypes={initiativeArchetypes}
|
||||
compact
|
||||
/>
|
||||
)}
|
||||
|
||||
<ul className="plan-outline-nav__list">
|
||||
<ul className="analysis-split__nav-list">
|
||||
{PLAN_OUTLINE_NODES.map((node) => {
|
||||
const needsInitiative = node.requiresInitiative && !initiativeId
|
||||
const isDisabled = node.disabled || needsInitiative
|
||||
|
|
@ -102,8 +72,8 @@ export function PlanOutlineNav() {
|
|||
<li key={node.key}>
|
||||
<span
|
||||
className={
|
||||
'plan-outline-nav__link plan-outline-nav__link--disabled' +
|
||||
(activeKey === node.key ? ' plan-outline-nav__link--active' : '')
|
||||
'analysis-split__nav-link analysis-split__nav-link--disabled' +
|
||||
(activeKey === node.key ? ' analysis-split__nav-link--active' : '')
|
||||
}
|
||||
aria-disabled="true"
|
||||
title={needsInitiative ? 'Vorhaben im Scope wählen' : node.hint}
|
||||
|
|
@ -120,8 +90,8 @@ export function PlanOutlineNav() {
|
|||
to={hrefWithScope(node.to)}
|
||||
end
|
||||
className={({ isActive }) =>
|
||||
'plan-outline-nav__link' +
|
||||
(isActive ? ' plan-outline-nav__link--active' : '')
|
||||
'analysis-split__nav-link' +
|
||||
(isActive ? ' analysis-split__nav-link--active' : '')
|
||||
}
|
||||
>
|
||||
{nodeLabel(node)}
|
||||
|
|
|
|||
|
|
@ -54,8 +54,6 @@ export function SteeringSnapshotPanel({
|
|||
method_label,
|
||||
archetype_key,
|
||||
archetype_label,
|
||||
method_profile_key,
|
||||
method_profile_label,
|
||||
steering_guidance,
|
||||
attention_items = [],
|
||||
signals = [],
|
||||
|
|
@ -117,13 +115,6 @@ export function SteeringSnapshotPanel({
|
|||
{(archetype_label || archetype_key) && (
|
||||
<p className="steering-archetype-row muted">
|
||||
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>
|
||||
)}
|
||||
{(method_label || canManageMethod) && (
|
||||
|
|
|
|||
24
frontend/src/config/controlNav.js
Normal file
24
frontend/src/config/controlNav.js
Normal 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
|
||||
}
|
||||
18
frontend/src/config/modeNav.test.js
Normal file
18
frontend/src/config/modeNav.test.js
Normal 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')
|
||||
})
|
||||
})
|
||||
24
frontend/src/config/workNav.js
Normal file
24
frontend/src/config/workNav.js
Normal 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
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ import { scopedPath } from '../../utils/routes.js'
|
|||
import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx'
|
||||
import { SteeringSnapshotPanel } from '../../components/SteeringSnapshotPanel.jsx'
|
||||
import { NextActionWidget } from '../../widgets/NextActionWidget.jsx'
|
||||
import { InitiativeNavCards } from '../../components/InitiativeNavCards.jsx'
|
||||
|
||||
export function InitiativeOverviewPage() {
|
||||
const {
|
||||
|
|
@ -16,9 +15,6 @@ export function InitiativeOverviewPage() {
|
|||
methodBusy,
|
||||
capabilities,
|
||||
handleMethodChange,
|
||||
backlogItems,
|
||||
roadmapItems,
|
||||
milestones,
|
||||
} = useInitiativeOperations()
|
||||
|
||||
const counts = steeringSnapshot?.counts || {}
|
||||
|
|
@ -66,15 +62,6 @@ export function InitiativeOverviewPage() {
|
|||
</Link>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<InitiativeNavCards
|
||||
initiativeId={initiativeId}
|
||||
counts={{
|
||||
backlog: backlogItems.length,
|
||||
roadmapItems: roadmapItems.length,
|
||||
actionsOpen: counts.actions_open,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,29 @@
|
|||
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 { ModeSubNav } from '../../components/ModeSubNav.jsx'
|
||||
|
||||
const CONTROL_NAV = [
|
||||
{ to: '/control/status', label: 'Steuerung', end: true },
|
||||
{ to: '/control/journey', label: 'Journey', end: true },
|
||||
]
|
||||
import {
|
||||
CONTROL_NAV_ITEMS,
|
||||
resolveControlNavActiveKey,
|
||||
} from '../../config/controlNav.js'
|
||||
|
||||
export function ControlLayout() {
|
||||
return (
|
||||
<div className="app-page">
|
||||
<ModeShell modeKey="control" />
|
||||
<ModeSubNav items={CONTROL_NAV} />
|
||||
<Outlet />
|
||||
<ModeAreaShell
|
||||
nav={
|
||||
<ModeAreaNav
|
||||
modeLabel="Kontrolle"
|
||||
items={CONTROL_NAV_ITEMS}
|
||||
resolveActiveKey={resolveControlNavActiveKey}
|
||||
showScope
|
||||
scopeMutedLabel="Vorhaben wählen"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Outlet />
|
||||
</ModeAreaShell>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,15 @@
|
|||
import { Outlet } from 'react-router-dom'
|
||||
import { ModeAreaShell } from '../../components/ModeAreaShell.jsx'
|
||||
import { ModeShell } from '../../components/ModeShell.jsx'
|
||||
import { PlanOutlineNav } from '../../components/PlanOutlineNav.jsx'
|
||||
|
||||
export function PlanLayout() {
|
||||
return (
|
||||
<div className="app-page plan-outline-shell">
|
||||
<div className="app-page">
|
||||
<ModeShell modeKey="plan" />
|
||||
<div className="plan-outline-shell__body">
|
||||
<PlanOutlineNav />
|
||||
<div className="plan-outline-shell__content">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
<ModeAreaShell nav={<PlanOutlineNav />}>
|
||||
<Outlet />
|
||||
</ModeAreaShell>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { LoadingState } from '../../components/LoadingState.jsx'
|
|||
import { Modal } from '../../components/Modal.jsx'
|
||||
import { InitiativeForm } from '../../components/InitiativeForm.jsx'
|
||||
import { InitiativeSteeringMeta } from '../../components/InitiativeSteeringMeta.jsx'
|
||||
import { MethodProfileSelect } from '../../components/MethodProfileSelect.jsx'
|
||||
import { PriorityBadge } from '../../components/PriorityBadge.jsx'
|
||||
import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx'
|
||||
import { useProgramScope } from '../../context/ProgramScopeContext.jsx'
|
||||
|
|
@ -88,8 +87,6 @@ function PlanProfileInner() {
|
|||
formBusy,
|
||||
steeringSnapshot,
|
||||
steeringMethods,
|
||||
methodBusy,
|
||||
handleMethodProfileChange,
|
||||
actors,
|
||||
projects,
|
||||
} = ops
|
||||
|
|
@ -143,16 +140,6 @@ function PlanProfileInner() {
|
|||
steeringSnapshot={steeringSnapshot}
|
||||
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">
|
||||
<h3 className="plan-profile__label">Verantwortlich (Owner)</h3>
|
||||
<p>{ownerName}</p>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,24 @@
|
|||
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 { ModeSubNav } from '../../components/ModeSubNav.jsx'
|
||||
|
||||
const WORK_NAV = [
|
||||
{ to: '/work/today', label: 'Heute', end: true },
|
||||
{ to: '/work/mine', label: 'Meine Queue', end: true },
|
||||
]
|
||||
import { WORK_NAV_ITEMS, resolveWorkNavActiveKey } from '../../config/workNav.js'
|
||||
|
||||
export function WorkLayout() {
|
||||
return (
|
||||
<div className="app-page">
|
||||
<ModeShell modeKey="work" />
|
||||
<ModeSubNav items={WORK_NAV} />
|
||||
<Outlet />
|
||||
<ModeAreaShell
|
||||
nav={
|
||||
<ModeAreaNav
|
||||
modeLabel="Ausführen"
|
||||
items={WORK_NAV_ITEMS}
|
||||
resolveActiveKey={resolveWorkNavActiveKey}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Outlet />
|
||||
</ModeAreaShell>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -223,28 +223,110 @@
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mode-subnav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
/* Mitai analysis-split: Bereichs-Shell mit linker Sub-Nav (Desktop) */
|
||||
.analysis-split {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(200px, 240px) minmax(0, 1fr);
|
||||
gap: 20px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.mode-subnav__link {
|
||||
padding: 8px 14px;
|
||||
border-radius: 999px;
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--jk-text-muted);
|
||||
.analysis-split__main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.analysis-split__nav {
|
||||
position: sticky;
|
||||
top: 12px;
|
||||
padding: 14px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--jk-border);
|
||||
background: var(--jk-surface);
|
||||
}
|
||||
|
||||
.mode-subnav__link--active {
|
||||
color: var(--jk-primary);
|
||||
border-color: var(--jk-primary);
|
||||
.analysis-split__nav-header {
|
||||
margin-bottom: 12px;
|
||||
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);
|
||||
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 {
|
||||
|
|
@ -292,93 +374,6 @@
|
|||
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 {
|
||||
display: flex;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user