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 && ( - + )} ) 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 ( + + ) +} 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;