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 ( +
+ +