-
Plan
+
Zielzustände (Gates)
- Roadmap-Elemente methodenneutral — Gates, Reifegrade oder Review-Punkte.
- Erreicht nur über Verify (Evidence, Review oder Decision).
+ Überprüfbare Zielpunkte — optional. Operative Planung läuft unter Ausführung.
+ Erreicht nur über Verify (Kriterien, Evidence, Review).
{canManage && (
diff --git a/frontend/src/components/TasksSection.jsx b/frontend/src/components/TasksSection.jsx
new file mode 100644
index 0000000..031ed1a
--- /dev/null
+++ b/frontend/src/components/TasksSection.jsx
@@ -0,0 +1,151 @@
+import { useCallback, useEffect, useState } from 'react'
+import {
+ createActionTask,
+ deleteTask,
+ listActionTasks,
+ updateTask,
+} from '../api/tasks.js'
+import { TASK_STATUSES, TASK_STATUS_LABELS } from '../constants/status.js'
+import { EmptyState } from './EmptyState.jsx'
+
+export function TasksSection({ actionId, canManage }) {
+ const [tasks, setTasks] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+ const [busy, setBusy] = useState(false)
+ const [title, setTitle] = useState('')
+
+ const load = useCallback(async () => {
+ if (!actionId) return
+ setLoading(true)
+ setError(null)
+ try {
+ setTasks(await listActionTasks(actionId))
+ } catch (err) {
+ setError(err.message)
+ } finally {
+ setLoading(false)
+ }
+ }, [actionId])
+
+ useEffect(() => {
+ load()
+ }, [load])
+
+ async function handleCreate(e) {
+ e.preventDefault()
+ if (!title.trim()) return
+ setBusy(true)
+ try {
+ await createActionTask(actionId, { title: title.trim() })
+ setTitle('')
+ await load()
+ } catch (err) {
+ setError(err.message)
+ } finally {
+ setBusy(false)
+ }
+ }
+
+ async function handleStatus(taskId, status) {
+ setBusy(true)
+ try {
+ await updateTask(taskId, { status })
+ await load()
+ } catch (err) {
+ setError(err.message)
+ } finally {
+ setBusy(false)
+ }
+ }
+
+ async function handleDelete(taskId) {
+ setBusy(true)
+ try {
+ await deleteTask(taskId)
+ await load()
+ } catch (err) {
+ setError(err.message)
+ } finally {
+ setBusy(false)
+ }
+ }
+
+ if (loading) return
Tasks werden geladen…
+
+ return (
+
+
+
+
Aufgaben
+
+ Kleinste ausführbare Schritte — später Zuordnung zu Gates (Zielerreichung).
+
+
+
+
+ {error && {error}
}
+
+ {canManage && (
+
+ )}
+
+ {tasks.length === 0 && (
+
+ )}
+
+
+
+ )
+}
diff --git a/frontend/src/constants/status.js b/frontend/src/constants/status.js
index a4acbf9..a413217 100644
--- a/frontend/src/constants/status.js
+++ b/frontend/src/constants/status.js
@@ -15,6 +15,7 @@ export const EVIDENCE_STATUSES = ['submitted', 'accepted', 'rejected']
export const DECISION_STATUSES = ['proposed', 'decided', 'superseded']
export const REVIEW_STATUSES = ['planned', 'completed', 'skipped']
export const RECURRING_STATUSES = ['active', 'paused', 'ended']
+export const TASK_STATUSES = ['open', 'in_progress', 'done', 'discarded']
export const PRIORITIES = ['low', 'normal', 'high']
export const OPEN_ACTION_STATUSES = ['open', 'ready', 'in_progress', 'blocked', 'review_required']
@@ -114,6 +115,13 @@ export const RECURRING_STATUS_LABELS = {
ended: 'Beendet',
}
+export const TASK_STATUS_LABELS = {
+ open: 'Offen',
+ in_progress: 'In Arbeit',
+ done: 'Erledigt',
+ discarded: 'Verworfen',
+}
+
export const PRIORITY_LABELS = {
low: 'Niedrig',
normal: 'Normal',
diff --git a/frontend/src/context/InitiativeOperationsContext.jsx b/frontend/src/context/InitiativeOperationsContext.jsx
index b4322c1..fb6be98 100644
--- a/frontend/src/context/InitiativeOperationsContext.jsx
+++ b/frontend/src/context/InitiativeOperationsContext.jsx
@@ -34,6 +34,11 @@ import {
deleteRoadmapItem,
verifyRoadmapItemReached,
} from '../api/roadmap.js'
+import {
+ listInitiativeProjects,
+ createInitiativeProject,
+ deleteProject,
+} from '../api/projects.js'
import {
listInitiativeEvidence,
createInitiativeEvidence,
@@ -76,6 +81,8 @@ export function InitiativeOperationsProvider({ children }) {
const [blockers, setBlockers] = useState([])
const [backlogItems, setBacklogItems] = useState([])
const [roadmapItems, setRoadmapItems] = useState([])
+ const [projects, setProjects] = useState([])
+ const [selectedProjectId, setSelectedProjectId] = useState('')
const [evidenceItems, setEvidenceItems] = useState([])
const [decisions, setDecisions] = useState([])
const [reviews, setReviews] = useState([])
@@ -112,6 +119,7 @@ export function InitiativeOperationsProvider({ children }) {
loads.push(listInitiativeBlockers(id).then(setBlockers).catch(() => setBlockers([])))
loads.push(listInitiativeBacklog(id).then(setBacklogItems).catch(() => setBacklogItems([])))
loads.push(listInitiativeRoadmapItems(id).then(setRoadmapItems).catch(() => setRoadmapItems([])))
+ loads.push(listInitiativeProjects(id).then(setProjects).catch(() => setProjects([])))
loads.push(listInitiativeEvidence(id).then(setEvidenceItems).catch(() => setEvidenceItems([])))
loads.push(listInitiativeDecisions(id).then(setDecisions).catch(() => setDecisions([])))
loads.push(listInitiativeReviews(id).then(setReviews).catch(() => setReviews([])))
@@ -147,11 +155,16 @@ export function InitiativeOperationsProvider({ children }) {
}, [load])
const visibleActions = useMemo(
- () =>
- hideDone
+ () => {
+ let list = hideDone
? actions.filter((a) => a.status !== 'done' && a.status !== 'discarded')
- : actions,
- [actions, hideDone]
+ : actions
+ if (selectedProjectId) {
+ list = list.filter((a) => a.project_id === selectedProjectId)
+ }
+ return list
+ },
+ [actions, hideDone, selectedProjectId]
)
const actionContextById = useMemo(
@@ -185,7 +198,11 @@ export function InitiativeOperationsProvider({ children }) {
: context?.actor?.id
? [context.actor.id]
: []
- await createInitiativeAction(id, { ...payload, assigned_actor_ids: assigned })
+ await createInitiativeAction(id, {
+ ...payload,
+ project_id: payload.project_id || selectedProjectId || undefined,
+ assigned_actor_ids: assigned,
+ })
setShowActionForm(false)
await load()
} catch (err) {
@@ -205,6 +222,8 @@ export function InitiativeOperationsProvider({ children }) {
priority: payload.priority,
due_at: payload.due_at,
clear_due_at: !payload.due_at,
+ project_id: payload.project_id,
+ clear_project: payload.project_id === '' || payload.project_id === null,
})
if (capabilities.has('kairo.action.manage')) {
await setActionAssignments(actionId, payload.assigned_actor_ids || [])
@@ -386,6 +405,28 @@ export function InitiativeOperationsProvider({ children }) {
}
}
+ async function handleCreateProject(body) {
+ setFormBusy(true)
+ try {
+ await createInitiativeProject(id, body)
+ await load()
+ } catch (err) {
+ setError(err.message)
+ } finally {
+ setFormBusy(false)
+ }
+ }
+
+ async function handleDeleteProject(projectId) {
+ try {
+ await deleteProject(projectId)
+ if (selectedProjectId === projectId) setSelectedProjectId('')
+ await load()
+ } catch (err) {
+ setError(err.message)
+ }
+ }
+
const milestones = useMemo(
() => roadmapItems.filter((item) => item.item_type === 'milestone'),
[roadmapItems]
@@ -520,6 +561,9 @@ export function InitiativeOperationsProvider({ children }) {
unlinkedBlockers,
backlogItems,
roadmapItems,
+ projects,
+ selectedProjectId,
+ setSelectedProjectId,
milestones,
evidenceItems,
decisions,
@@ -567,7 +611,9 @@ export function InitiativeOperationsProvider({ children }) {
handleDeleteRoadmapItem,
handleCreateMilestone: handleCreateRoadmapItem,
handleMilestoneStatus: handleRoadmapItemStatus,
- handleDeleteMilestone: handleDeleteRoadmapItem,
+ handleDeleteMilestone: handleDeleteRoadmapItem,
+ handleCreateProject,
+ handleDeleteProject,
handleCreateEvidence,
handleEvidenceStatus,
handleDeleteEvidence,
diff --git a/frontend/src/pages/ActionDetailPage.jsx b/frontend/src/pages/ActionDetailPage.jsx
index bae6aaa..88a8ed9 100644
--- a/frontend/src/pages/ActionDetailPage.jsx
+++ b/frontend/src/pages/ActionDetailPage.jsx
@@ -2,10 +2,12 @@ import { useCallback, useEffect, useState } from 'react'
import { Link, useParams } from 'react-router-dom'
import { getAction } from '../api/actions.js'
import { getInitiativeSteeringSnapshot } from '../api/initiatives.js'
+import { listInitiativeProjects } from '../api/projects.js'
import { updateAction, setActionAssignments } from '../api/actions.js'
import { createInitiativeBlocker } from '../api/blockers.js'
import { ActionHubCard } from '../components/ActionHubCard.jsx'
import { ActionForm } from '../components/ActionForm.jsx'
+import { TasksSection } from '../components/TasksSection.jsx'
import { ErrorState } from '../components/ErrorState.jsx'
import { LoadingState } from '../components/LoadingState.jsx'
import { useInitiativeOperations } from '../context/InitiativeOperationsContext.jsx'
@@ -24,11 +26,13 @@ function ActionDetailBody({
formBusy,
capabilities,
actorsState,
+ projects = [],
}) {
if (editing) {
return (
+ <>
+
+
+ >
)
}
@@ -70,6 +80,7 @@ function ActionDetailStandalone() {
const { capabilities } = useCapabilities()
const actorsState = useActors()
const [action, setAction] = useState(null)
+ const [projects, setProjects] = useState([])
const [context, setContext] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
@@ -82,7 +93,11 @@ function ActionDetailStandalone() {
try {
const actionData = await getAction(actionId)
setAction(actionData)
- const snap = await getInitiativeSteeringSnapshot(actionData.initiative_id)
+ const [snap, projectData] = await Promise.all([
+ getInitiativeSteeringSnapshot(actionData.initiative_id),
+ listInitiativeProjects(actionData.initiative_id).catch(() => []),
+ ])
+ setProjects(projectData)
setContext((snap.actions || []).find((a) => a.id === actionId) || null)
} catch (err) {
setError(err.message)
@@ -188,6 +203,7 @@ function ActionDetailStandalone() {
formBusy={formBusy}
capabilities={capabilities}
actorsState={actorsState}
+ projects={projects}
/>
@@ -228,6 +244,7 @@ function ActionDetailNested() {
usedFallback: ops.actorsUsedFallback,
reload: ops.reloadActors,
}}
+ projects={ops.projects}
/>