diff --git a/backend/version.py b/backend/version.py
index d064670..15e34a5 100644
--- a/backend/version.py
+++ b/backend/version.py
@@ -1,3 +1,3 @@
-APP_VERSION = "0.15.0-ap1.6"
+APP_VERSION = "0.15.1-ap1.5e"
DB_SCHEMA_VERSION = "014"
APP_NAME = "jinkendo-kairo"
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 7eb2bb6..0137e1f 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -7,6 +7,7 @@ import { ActionDetailPage } from './pages/ActionDetailPage.jsx'
import { InitiativeOverviewPage } from './pages/initiative/InitiativeOverviewPage.jsx'
import { InitiativePlanPage } from './pages/initiative/InitiativePlanPage.jsx'
import { InitiativeExecutionPage } from './pages/initiative/InitiativeExecutionPage.jsx'
+import { ProjectDetailPage } from './pages/initiative/ProjectDetailPage.jsx'
import { InitiativeInboxPage } from './pages/initiative/InitiativeInboxPage.jsx'
import { InitiativeJourneyPage } from './pages/initiative/InitiativeJourneyPage.jsx'
import { RoadmapItemDetailPage } from './pages/initiative/RoadmapItemDetailPage.jsx'
@@ -42,6 +43,7 @@ function AppRoutes() {
} />
} />
} />
+ } />
} />
} />
} />
diff --git a/frontend/src/api/projects.js b/frontend/src/api/projects.js
index 490d84c..5e2f27c 100644
--- a/frontend/src/api/projects.js
+++ b/frontend/src/api/projects.js
@@ -4,6 +4,10 @@ export function listInitiativeProjects(initiativeId) {
return apiFetch(`/api/initiatives/${initiativeId}/projects`)
}
+export function getProject(projectId) {
+ return apiFetch(`/api/projects/${projectId}`)
+}
+
export function createInitiativeProject(initiativeId, body) {
return apiFetch(`/api/initiatives/${initiativeId}/projects`, {
method: 'POST',
diff --git a/frontend/src/components/InitiativeLifecycleBar.jsx b/frontend/src/components/InitiativeLifecycleBar.jsx
new file mode 100644
index 0000000..d774721
--- /dev/null
+++ b/frontend/src/components/InitiativeLifecycleBar.jsx
@@ -0,0 +1,81 @@
+import { LIFECYCLE_LABELS } from '../constants/operating.js'
+import { Link } from 'react-router-dom'
+
+export function InitiativeLifecycleBar({
+ snapshot,
+ loading,
+ error,
+ initiativeId,
+ compact = false,
+}) {
+ if (loading) {
+ return (
+
+ Steuerungszustand wird geladen …
+
+ )
+ }
+
+ if (error || !snapshot) {
+ return (
+
+
+ Steuerungszustand nicht verfügbar — siehe Tab{' '}
+ Steuerung.
+
+
+ )
+ }
+
+ const displayLabel =
+ snapshot.lifecycle_label ||
+ LIFECYCLE_LABELS[snapshot.lifecycle_state] ||
+ snapshot.lifecycle_state
+
+ const stats = [
+ { label: 'Offen', value: snapshot.counts?.actions_open ?? 0, warn: false },
+ {
+ label: 'Blockiert',
+ value: snapshot.counts?.actions_blocked ?? 0,
+ warn: (snapshot.counts?.actions_blocked ?? 0) > 0,
+ },
+ {
+ label: 'Review',
+ value: snapshot.counts?.actions_review_required ?? 0,
+ warn: (snapshot.counts?.actions_review_required ?? 0) > 0,
+ },
+ {
+ label: 'Blocker',
+ value: snapshot.counts?.blockers_open ?? 0,
+ warn: (snapshot.counts?.blockers_open ?? 0) > 0,
+ },
+ ]
+
+ return (
+
+
+
+
Programm-Fortschritt
+
+ {displayLabel}
+
+
+
+ Steuerung
+
+
+
+ {stats.map(({ label, value, warn }) => (
+
+ {value} {label}
+
+ ))}
+
+
+ )
+}
diff --git a/frontend/src/components/ProjectForm.jsx b/frontend/src/components/ProjectForm.jsx
new file mode 100644
index 0000000..7bbebf5
--- /dev/null
+++ b/frontend/src/components/ProjectForm.jsx
@@ -0,0 +1,140 @@
+import { INITIATIVE_STATUSES, INITIATIVE_STATUS_LABELS } from '../constants/status.js'
+import { GateSelect } from './GateSelect.jsx'
+import { flattenProjectOptions } from '../utils/projectTree.js'
+
+const CONTAINER_KINDS = [
+ { value: '', label: '— Standard —' },
+ { value: 'project', label: 'Projekt' },
+ { value: 'stream', label: 'Stream' },
+ { value: 'phase', label: 'Phase' },
+ { value: 'release', label: 'Release' },
+]
+
+export function ProjectForm({
+ initial = {},
+ projects = [],
+ roadmapItems = [],
+ excludeProjectId = '',
+ onSubmit,
+ onCancel,
+ busy = false,
+ submitLabel = 'Speichern',
+}) {
+ const parentOptions = flattenProjectOptions(
+ projects.filter((p) => p.id !== excludeProjectId),
+ )
+
+ async function handleSubmit(e) {
+ e.preventDefault()
+ const form = e.target
+ const parentValue = form.parent_project_id?.value ?? ''
+ const kindValue = form.container_kind?.value ?? ''
+ const gateValue = form.roadmap_item_id?.value ?? ''
+ const targetValue = form.target_date?.value ?? ''
+ const sortValue = form.sort_order?.value ?? '0'
+
+ await onSubmit({
+ title: form.title.value.trim(),
+ description: form.description.value,
+ status: form.status.value,
+ parent_project_id: parentValue || undefined,
+ clear_parent_project: parentValue === '',
+ container_kind: kindValue || undefined,
+ clear_container_kind: kindValue === '',
+ roadmap_item_id: gateValue || undefined,
+ clear_roadmap_item: gateValue === '',
+ target_date: targetValue || undefined,
+ clear_target_date: !targetValue,
+ sort_order: Number.parseInt(sortValue, 10) || 0,
+ })
+ }
+
+ return (
+
+ )
+}
diff --git a/frontend/src/components/ProjectsSection.jsx b/frontend/src/components/ProjectsSection.jsx
index b28a7d9..6f7a239 100644
--- a/frontend/src/components/ProjectsSection.jsx
+++ b/frontend/src/components/ProjectsSection.jsx
@@ -1,18 +1,18 @@
-import { useMemo, useState } from 'react'
+import { Link } from 'react-router-dom'
+import { useMemo } from 'react'
import { EmptyState } from './EmptyState.jsx'
import { StatusBadge } from './StatusBadge.jsx'
import {
buildProjectsByParent,
containerKindLabel,
- flattenProjectOptions,
} from '../utils/projectTree.js'
-import { GateSelect, gateTitleById } from './GateSelect.jsx'
+import { gateTitleById } from './GateSelect.jsx'
function ProjectTreeNodes({
projects,
roadmapItems,
+ initiativeId,
parentId,
- depth,
selectedProjectId,
onSelectProject,
canManage,
@@ -28,17 +28,16 @@ function ProjectTreeNodes({
{nodes.map((project) => (
-
-
-
-