Kairo-Jinkendo/frontend/src/components/PlanOutlineNav.jsx
Lars 4f2f801889
Some checks failed
Deploy Development / deploy (push) Successful in 56s
Test Suite / pytest-backend (push) Failing after 1m48s
Test Suite / k6 /api/health Baseline (push) Has been skipped
Test Suite / playwright-smoke (push) Has been skipped
Test Suite / lint-backend (push) Successful in 2s
Test Suite / compose-smoke (push) Has been skipped
AP1.12d: Plan-Outline-Knoten Arbeit mit Actions-Liste im Plan-Kontext.
Route /plan/work, Modal-Anlage, Links zu Objektseiten; Outline zeigt Zähler für Eingang und Arbeit.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 11:38:17 +02:00

106 lines
3.3 KiB
JavaScript

import { NavLink, useLocation } from 'react-router-dom'
import { useEffect, useState } from 'react'
import { listInitiativeActions } from '../api/initiatives.js'
import { listInitiativeBacklog } from '../api/backlog.js'
import { useProgramScope } from '../context/ProgramScopeContext.jsx'
import {
PLAN_OUTLINE_NODES,
resolvePlanOutlineActiveKey,
} from '../plan/planOutlineNodes.js'
export function PlanOutlineNav() {
const location = useLocation()
const { initiativeId, initiativeTitle, hrefWithScope } = useProgramScope()
const activeKey = resolvePlanOutlineActiveKey(location.pathname)
const [counts, setCounts] = useState({ inbox: null, work: null })
useEffect(() => {
if (!initiativeId) {
setCounts({ inbox: null, work: null })
return undefined
}
let cancelled = false
Promise.all([
listInitiativeBacklog(initiativeId).catch(() => []),
listInitiativeActions(initiativeId).catch(() => []),
]).then(([backlog, actions]) => {
if (cancelled) return
setCounts({
inbox: backlog.filter((item) => item.status !== 'converted').length,
work: actions.filter(
(action) => action.status !== 'done' && action.status !== 'discarded',
).length,
})
})
return () => {
cancelled = true
}
}, [initiativeId, location.pathname])
function nodeLabel(node) {
if (node.key === 'inbox' && counts.inbox != null) {
return `${node.label} (${counts.inbox})`
}
if (node.key === 'work' && counts.work != null) {
return `${node.label} (${counts.work})`
}
return node.label
}
return (
<nav className="plan-outline-nav" aria-label="Plan-Outline">
<div className="plan-outline-nav__header">
<span className="plan-outline-nav__eyebrow">Planen</span>
{initiativeId ? (
<p className="plan-outline-nav__root" title={initiativeTitle || initiativeId}>
{initiativeTitle || 'Vorhaben'}
</p>
) : (
<p className="plan-outline-nav__root plan-outline-nav__root--muted">
Portfolio — Vorhaben wählen
</p>
)}
</div>
<ul className="plan-outline-nav__list">
{PLAN_OUTLINE_NODES.map((node) => {
const needsInitiative = node.requiresInitiative && !initiativeId
const isDisabled = node.disabled || needsInitiative
if (isDisabled) {
return (
<li key={node.key}>
<span
className={
'plan-outline-nav__link plan-outline-nav__link--disabled' +
(activeKey === node.key ? ' plan-outline-nav__link--active' : '')
}
aria-disabled="true"
title={needsInitiative ? 'Vorhaben im Scope wählen' : node.hint}
>
{nodeLabel(node)}
</span>
</li>
)
}
return (
<li key={node.key}>
<NavLink
to={hrefWithScope(node.to)}
end
className={({ isActive }) =>
'plan-outline-nav__link' +
(isActive ? ' plan-outline-nav__link--active' : '')
}
>
{nodeLabel(node)}
</NavLink>
</li>
)
})}
</ul>
</nav>
)
}