From 33831a8c2c771384c5574001c7ca8eb1260e7db4 Mon Sep 17 00:00:00 2001 From: Lars Date: Wed, 8 Jul 2026 11:06:10 +0200 Subject: [PATCH] =?UTF-8?q?AP1.5e:=20Projekt-Detail,=20Bearbeitung=20und?= =?UTF-8?q?=20Lifecycle=20in=20der=20Ausf=C3=BChrung.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Detail-Route mit vollem Formular, Struktur-Navigator statt Inline-CRUD, Lifecycle sichtbar im Vorhaben-Header und auf Ausführung (0.15.1-ap1.5e). --- backend/version.py | 2 +- frontend/src/App.jsx | 2 + frontend/src/api/projects.js | 4 + .../src/components/InitiativeLifecycleBar.jsx | 81 +++++ frontend/src/components/ProjectForm.jsx | 140 ++++++++ frontend/src/components/ProjectsSection.jsx | 235 ++++--------- frontend/src/constants/status.js | 10 + .../context/InitiativeOperationsContext.jsx | 25 +- frontend/src/layout/InitiativeLayout.jsx | 13 +- .../initiative/InitiativeExecutionPage.jsx | 14 +- .../pages/initiative/ProjectDetailPage.jsx | 326 ++++++++++++++++++ frontend/src/styles/components.css | 137 ++++++++ frontend/src/utils/projectTree.js | 11 + 13 files changed, 827 insertions(+), 173 deletions(-) create mode 100644 frontend/src/components/InitiativeLifecycleBar.jsx create mode 100644 frontend/src/components/ProjectForm.jsx create mode 100644 frontend/src/pages/initiative/ProjectDetailPage.jsx 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 ( +
+ +