diff --git a/frontend/src/components/PlanOutlineNav.jsx b/frontend/src/components/PlanOutlineNav.jsx
index 2d3c21d..404841c 100644
--- a/frontend/src/components/PlanOutlineNav.jsx
+++ b/frontend/src/components/PlanOutlineNav.jsx
@@ -5,13 +5,14 @@ import { listInitiativeActions } from '../api/initiatives.js'
import { listInitiativeBacklog } from '../api/backlog.js'
import { useProgramScope } from '../context/ProgramScopeContext.jsx'
import { buildProjectsByParent } from '../utils/projectTree.js'
-import { projectPath, actionPath } from '../utils/routes.js'
+import { projectPath } from '../utils/routes.js'
import {
PLAN_OUTLINE_NODES,
resolvePlanOutlineActiveKey,
resolvePlanOutlineProjectId,
resolvePlanOutlineActionId,
} from '../plan/planOutlineNodes.js'
+import { PlanOutlineWorkTree } from './PlanOutlineWorkTree.jsx'
function PlanOutlineProjectTree({ projects, parentId, activeProjectId, depth = 0 }) {
const byParent = useMemo(() => buildProjectsByParent(projects), [projects])
@@ -217,23 +218,11 @@ export function PlanOutlineNav() {
/>
)}
{hasActionList && workOpen && (
-
- {openActions.map((action) => (
- -
-
- 'analysis-split__nav-link analysis-split__nav-link--nested' +
- (isActive || activeActionId === action.id
- ? ' analysis-split__nav-link--active'
- : '')
- }
- >
- {action.title}
-
-
- ))}
-
+
)}
)
diff --git a/frontend/src/components/PlanOutlineWorkTree.jsx b/frontend/src/components/PlanOutlineWorkTree.jsx
new file mode 100644
index 0000000..be085c1
--- /dev/null
+++ b/frontend/src/components/PlanOutlineWorkTree.jsx
@@ -0,0 +1,188 @@
+import { useEffect, useMemo, useState } from 'react'
+import { NavLink } from 'react-router-dom'
+import { listActionTasks } from '../api/tasks.js'
+import { actionPath } from '../utils/routes.js'
+import { buildTasksByParent, countOpenTasks, MAX_TASK_UI_DEPTH } from '../utils/taskTree.js'
+
+function actionTasksHref(actionId) {
+ return `${actionPath(actionId)}#tasks`
+}
+
+function PlanOutlineTaskTree({
+ tasksByParent,
+ parentId,
+ actionId,
+ depth,
+ expandedTaskIds,
+ onToggleTask,
+ activeActionId,
+}) {
+ const nodes = (tasksByParent.get(parentId || '') || []).filter(
+ (task) => task.status !== 'done' && task.status !== 'discarded',
+ )
+
+ if (nodes.length === 0 || depth > MAX_TASK_UI_DEPTH) {
+ return null
+ }
+
+ return (
+
+ )
+}
+
+function PlanOutlineActionNode({ action, tasks, activeActionId, defaultExpanded }) {
+ const [actionOpen, setActionOpen] = useState(defaultExpanded)
+ const [expandedTaskIds, setExpandedTaskIds] = useState(() => new Set())
+
+ const tasksByParent = useMemo(() => buildTasksByParent(tasks || []), [tasks])
+ const openTaskCount = useMemo(() => countOpenTasks(tasks || []), [tasks])
+ const hasOpenTasks = openTaskCount > 0
+
+ useEffect(() => {
+ setActionOpen(defaultExpanded)
+ }, [defaultExpanded])
+
+ function toggleTask(taskId) {
+ setExpandedTaskIds((prev) => {
+ const next = new Set(prev)
+ if (next.has(taskId)) next.delete(taskId)
+ else next.add(taskId)
+ return next
+ })
+ }
+
+ return (
+
+
+ {hasOpenTasks && (
+
+ )}
+
+ 'analysis-split__nav-link analysis-split__nav-link--nested' +
+ (isActive || activeActionId === action.id
+ ? ' analysis-split__nav-link--active'
+ : '')
+ }
+ >
+ {action.title}
+ {openTaskCount > 0 && (
+ ({openTaskCount})
+ )}
+
+
+ {hasOpenTasks && actionOpen && (
+
+ )}
+
+ )
+}
+
+export function PlanOutlineWorkTree({ openActions, activeActionId, enabled }) {
+ const [tasksByAction, setTasksByAction] = useState({})
+ const actionIdsKey = useMemo(
+ () => openActions.map((action) => action.id).join(','),
+ [openActions],
+ )
+
+ useEffect(() => {
+ if (!enabled || openActions.length === 0) {
+ setTasksByAction({})
+ return undefined
+ }
+ let cancelled = false
+ Promise.all(
+ openActions.map((action) =>
+ listActionTasks(action.id)
+ .then((tasks) => [action.id, Array.isArray(tasks) ? tasks : []])
+ .catch(() => [action.id, []]),
+ ),
+ ).then((entries) => {
+ if (!cancelled) setTasksByAction(Object.fromEntries(entries))
+ })
+ return () => {
+ cancelled = true
+ }
+ }, [enabled, actionIdsKey, openActions])
+
+ return (
+
+ {openActions.map((action) => (
+
+ ))}
+
+ )
+}
diff --git a/frontend/src/components/TasksSection.jsx b/frontend/src/components/TasksSection.jsx
index 1710d8b..41ceaf3 100644
--- a/frontend/src/components/TasksSection.jsx
+++ b/frontend/src/components/TasksSection.jsx
@@ -95,7 +95,7 @@ export function TasksSection({ actionId, canManage, roadmapItems = [] }) {
if (loading) return Tasks werden geladen…
return (
-
+
Aufgaben
diff --git a/frontend/src/styles/program-chrome.css b/frontend/src/styles/program-chrome.css
index 47cdabe..02d426b 100644
--- a/frontend/src/styles/program-chrome.css
+++ b/frontend/src/styles/program-chrome.css
@@ -348,6 +348,17 @@
font-weight: 500;
}
+.analysis-split__nav-link--depth-2 {
+ font-size: 11px;
+ font-weight: 500;
+ opacity: 0.92;
+}
+
+.analysis-split__nav-meta {
+ font-weight: 400;
+ opacity: 0.85;
+}
+
@media (max-width: 900px) {
.analysis-split {
grid-template-columns: 1fr;