diff --git a/frontend/src/components/PlanOutlineNav.jsx b/frontend/src/components/PlanOutlineNav.jsx index c61bd84..2d3c21d 100644 --- a/frontend/src/components/PlanOutlineNav.jsx +++ b/frontend/src/components/PlanOutlineNav.jsx @@ -5,11 +5,12 @@ 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 } from '../utils/routes.js' +import { projectPath, actionPath } from '../utils/routes.js' import { PLAN_OUTLINE_NODES, resolvePlanOutlineActiveKey, resolvePlanOutlineProjectId, + resolvePlanOutlineActionId, } from '../plan/planOutlineNodes.js' function PlanOutlineProjectTree({ projects, parentId, activeProjectId, depth = 0 }) { @@ -52,16 +53,25 @@ export function PlanOutlineNav() { const { initiativeId, initiativeTitle, hrefWithScope } = useProgramScope() const activeKey = resolvePlanOutlineActiveKey(location.pathname) const activeProjectId = resolvePlanOutlineProjectId(location.pathname) + const activeActionId = resolvePlanOutlineActionId(location.pathname) const [counts, setCounts] = useState({ inbox: null, work: null }) const [projects, setProjects] = useState([]) + const [openActions, setOpenActions] = useState([]) const [structureOpen, setStructureOpen] = useState( activeKey === 'structure' || Boolean(activeProjectId), ) + const [workOpen, setWorkOpen] = useState( + activeKey === 'work' || Boolean(activeActionId), + ) useEffect(() => { setStructureOpen(activeKey === 'structure' || Boolean(activeProjectId)) }, [activeKey, activeProjectId]) + useEffect(() => { + setWorkOpen(activeKey === 'work' || Boolean(activeActionId)) + }, [activeKey, activeActionId]) + useEffect(() => { if (!initiativeId) { setProjects([]) @@ -91,11 +101,13 @@ export function PlanOutlineNav() { listInitiativeActions(initiativeId).catch(() => []), ]).then(([backlog, actions]) => { if (cancelled) return + const open = actions.filter( + (action) => action.status !== 'done' && action.status !== 'discarded', + ) + setOpenActions(open) setCounts({ inbox: backlog.filter((item) => item.status !== 'converted').length, - work: actions.filter( - (action) => action.status !== 'done' && action.status !== 'discarded', - ).length, + work: open.length, }) }) return () => { @@ -136,7 +148,9 @@ export function PlanOutlineNav() { const needsInitiative = node.requiresInitiative && !initiativeId const isDisabled = node.disabled || needsInitiative const isStructure = node.key === 'structure' + const isWork = node.key === 'work' const hasProjectTree = isStructure && projects.length > 0 && initiativeId + const hasActionList = isWork && openActions.length > 0 && initiativeId if (isDisabled) { return ( @@ -169,12 +183,25 @@ export function PlanOutlineNav() { {structureOpen ? '▾' : '▸'} )} + {hasActionList && ( + + )} 'analysis-split__nav-link' + - (isActive || (isStructure && activeProjectId) + (isActive || + (isStructure && activeProjectId) || + (isWork && activeActionId) ? ' analysis-split__nav-link--active' : '') } @@ -189,6 +216,25 @@ export function PlanOutlineNav() { activeProjectId={activeProjectId} /> )} + {hasActionList && workOpen && ( + + )} ) })} diff --git a/frontend/src/components/RoadmapItemForm.jsx b/frontend/src/components/RoadmapItemForm.jsx new file mode 100644 index 0000000..3304f3a --- /dev/null +++ b/frontend/src/components/RoadmapItemForm.jsx @@ -0,0 +1,124 @@ +import { + MILESTONE_STATUSES, + MILESTONE_STATUS_LABELS, + ROADMAP_ITEM_TYPE_LABELS, + SEQUENCING_MODE_LABELS, +} from '../constants/status.js' + +const ITEM_TYPES = ['milestone', 'review_gate', 'maturity_stage'] +const SEQUENCING_MODES = ['sequential', 'parallel', 'optional'] +const EDITABLE_STATUSES = MILESTONE_STATUSES.filter( + (s) => !['reached', 'moved', 'discarded'].includes(s), +) + +export function RoadmapItemForm({ + initial = {}, + onSubmit, + onCancel, + busy = false, + submitLabel = 'Anlegen', + mode = 'create', +}) { + async function handleSubmit(e) { + e.preventDefault() + const form = e.target + const payload = { + title: form.title.value.trim(), + goal_description: form.goal_description.value.trim(), + target_date: form.target_date.value || undefined, + item_type: form.item_type.value, + sequencing_mode: form.sequencing_mode.value, + } + if (mode === 'edit') { + payload.status = form.status.value + } + await onSubmit(payload) + } + + const statusLocked = ['reached', 'moved', 'discarded'].includes(initial.status) + + return ( +
+ {mode === 'create' && ( + + )} + +