diff --git a/backend/version.py b/backend/version.py index ca9baec..c191a33 100644 --- a/backend/version.py +++ b/backend/version.py @@ -1,3 +1,3 @@ -APP_VERSION = "0.16.7-ap1.12c" +APP_VERSION = "0.16.8-ap1.12d" DB_SCHEMA_VERSION = "015" APP_NAME = "jinkendo-kairo" diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 93b8a38..2fc76f9 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -73,6 +73,7 @@ function AppRoutes() { } /> } /> } /> + } /> }> diff --git a/frontend/src/components/PlanActionsSection.jsx b/frontend/src/components/PlanActionsSection.jsx new file mode 100644 index 0000000..ec02c9f --- /dev/null +++ b/frontend/src/components/PlanActionsSection.jsx @@ -0,0 +1,151 @@ +import { Link } from 'react-router-dom' +import { useMemo, useState } from 'react' +import { EmptyState } from './EmptyState.jsx' +import { StatusBadge } from './StatusBadge.jsx' +import { PriorityBadge } from './PriorityBadge.jsx' +import { Modal } from './Modal.jsx' +import { ActionForm } from './ActionForm.jsx' +import { gateTitleById } from './GateSelect.jsx' +import { buildProjectPath, collectProjectSubtreeIds } from '../utils/projectTree.js' +import { actionPath } from '../utils/routes.js' + +function formatProjectContext(projects, projectId) { + if (!projectId) return 'Direkt am Vorhaben' + const path = buildProjectPath(projects, projectId) + return path.map((p) => p.title).join(' → ') +} + +export function PlanActionsSection({ + actions, + projects = [], + roadmapItems = [], + scopeProjectId = '', + hideDone, + onHideDoneChange, + canManage, + onCreateAction, + actors, + actorsLoading, + actorsError, + actorsUsedFallback, + onReloadActors, + busy, +}) { + const [showCreate, setShowCreate] = useState(false) + + const visibleActions = useMemo(() => { + let list = hideDone + ? actions.filter((a) => a.status !== 'done' && a.status !== 'discarded') + : [...actions] + + if (scopeProjectId) { + const subtreeIds = collectProjectSubtreeIds(projects, scopeProjectId) + list = list.filter((a) => a.project_id && subtreeIds.has(a.project_id)) + } + + return list.sort((a, b) => { + const projectA = a.project_id || '' + const projectB = b.project_id || '' + if (projectA !== projectB) { + if (!projectA) return -1 + if (!projectB) return 1 + return projectA.localeCompare(projectB) + } + return (a.title || '').localeCompare(b.title || '') + }) + }, [actions, hideDone, scopeProjectId, projects]) + + async function handleCreate(payload) { + await onCreateAction(payload) + setShowCreate(false) + } + + return ( +
+
+
+

Arbeit

+

+ Committete Arbeitspakete im Plan-Kontext — Detail und Ausführung über die + Objektseite. +

+
+
+ + {canManage && ( + + )} +
+
+ + {scopeProjectId && ( +

+ Gefiltert nach Projekt im Scope — Scope oben leeren für alle Arbeitspakete. +

+ )} + + {visibleActions.length === 0 && ( + + )} + +
    + {visibleActions.map((action) => ( +
  • +
    + + {action.title} + +

    + {formatProjectContext(projects, action.project_id)} +

    + {action.roadmap_item_id && ( +

    + Gate: {gateTitleById(roadmapItems, action.roadmap_item_id)} +

    + )} + {action.description && ( +

    {action.description}

    + )} +
    +
    + + + + Öffnen + +
    +
  • + ))} +
+ + setShowCreate(false)} size="lg"> + setShowCreate(false)} + busy={busy} + submitLabel="Anlegen" + /> + +
+ ) +} diff --git a/frontend/src/components/PlanOutlineNav.jsx b/frontend/src/components/PlanOutlineNav.jsx index 7789bb9..3291933 100644 --- a/frontend/src/components/PlanOutlineNav.jsx +++ b/frontend/src/components/PlanOutlineNav.jsx @@ -1,4 +1,7 @@ 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, @@ -9,6 +12,40 @@ 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 (