+
+
Entscheidungen
+ {canManage && (
+
+ )}
+
+
+ {showForm && canManage && (
+
+ )}
+
+ {items.length === 0 && }
+
+
+
+ )
+}
diff --git a/frontend/src/components/EvidenceSection.jsx b/frontend/src/components/EvidenceSection.jsx
new file mode 100644
index 0000000..4e749c4
--- /dev/null
+++ b/frontend/src/components/EvidenceSection.jsx
@@ -0,0 +1,90 @@
+import { useState } from 'react'
+import { EVIDENCE_STATUSES, EVIDENCE_STATUS_LABELS } from '../constants/status.js'
+import { StatusBadge } from './StatusBadge.jsx'
+import { EmptyState } from './EmptyState.jsx'
+
+export function EvidenceSection({ items, canManage, onCreate, onUpdateStatus, onDelete, busy }) {
+ const [title, setTitle] = useState('')
+ const [showForm, setShowForm] = useState(false)
+
+ async function handleSubmit(e) {
+ e.preventDefault()
+ if (!title.trim()) return
+ await onCreate({ title: title.trim() })
+ setTitle('')
+ setShowForm(false)
+ }
+
+ return (
+
+
+
Evidence
+ {canManage && (
+
+ )}
+
+
+ {showForm && canManage && (
+
+ )}
+
+ {items.length === 0 && }
+
+
+
+ )
+}
diff --git a/frontend/src/components/RecurringSection.jsx b/frontend/src/components/RecurringSection.jsx
new file mode 100644
index 0000000..415ba81
--- /dev/null
+++ b/frontend/src/components/RecurringSection.jsx
@@ -0,0 +1,114 @@
+import { useState } from 'react'
+import { RECURRING_STATUSES, RECURRING_STATUS_LABELS } from '../constants/status.js'
+import { StatusBadge } from './StatusBadge.jsx'
+import { EmptyState } from './EmptyState.jsx'
+
+function formatDueAt(iso) {
+ if (!iso) return null
+ try {
+ return new Date(iso).toLocaleString('de-DE')
+ } catch {
+ return iso
+ }
+}
+
+export function RecurringSection({ items, canManage, onCreate, onUpdateStatus, onDelete, busy }) {
+ const [title, setTitle] = useState('')
+ const [nextDueAt, setNextDueAt] = useState('')
+ const [showForm, setShowForm] = useState(false)
+
+ async function handleSubmit(e) {
+ e.preventDefault()
+ if (!title.trim()) return
+ await onCreate({
+ title: title.trim(),
+ next_due_at: nextDueAt ? new Date(nextDueAt).toISOString() : null,
+ })
+ setTitle('')
+ setNextDueAt('')
+ setShowForm(false)
+ }
+
+ return (
+
+
+
Wiederkehrend
+ {canManage && (
+
+ )}
+
+
+ {showForm && canManage && (
+
+ )}
+
+ {items.length === 0 && }
+
+
+
+ )
+}
diff --git a/frontend/src/components/ReviewsSection.jsx b/frontend/src/components/ReviewsSection.jsx
new file mode 100644
index 0000000..8ca078c
--- /dev/null
+++ b/frontend/src/components/ReviewsSection.jsx
@@ -0,0 +1,114 @@
+import { useState } from 'react'
+import { REVIEW_STATUSES, REVIEW_STATUS_LABELS } from '../constants/status.js'
+import { StatusBadge } from './StatusBadge.jsx'
+import { EmptyState } from './EmptyState.jsx'
+
+function formatDueAt(iso) {
+ if (!iso) return null
+ try {
+ return new Date(iso).toLocaleString('de-DE')
+ } catch {
+ return iso
+ }
+}
+
+export function ReviewsSection({ items, canManage, onCreate, onUpdateStatus, onDelete, busy }) {
+ const [title, setTitle] = useState('')
+ const [dueAt, setDueAt] = useState('')
+ const [showForm, setShowForm] = useState(false)
+
+ async function handleSubmit(e) {
+ e.preventDefault()
+ if (!title.trim()) return
+ await onCreate({
+ title: title.trim(),
+ due_at: dueAt ? new Date(dueAt).toISOString() : null,
+ })
+ setTitle('')
+ setDueAt('')
+ setShowForm(false)
+ }
+
+ return (
+
+
+
Reviews
+ {canManage && (
+
+ )}
+
+
+ {showForm && canManage && (
+
+ )}
+
+ {items.length === 0 && }
+
+
+
+ )
+}
diff --git a/frontend/src/components/StatusBadge.jsx b/frontend/src/components/StatusBadge.jsx
index 37b78aa..1e2739f 100644
--- a/frontend/src/components/StatusBadge.jsx
+++ b/frontend/src/components/StatusBadge.jsx
@@ -22,6 +22,17 @@ const VARIANTS = {
resolved: 'status-done',
accepted_risk: 'status-paused',
dismissed: 'status-discarded',
+ ready: 'status-active',
+ review_required: 'status-progress',
+ submitted: 'status-open',
+ accepted: 'status-done',
+ rejected: 'status-discarded',
+ proposed: 'status-open',
+ decided: 'status-done',
+ superseded: 'status-paused',
+ completed: 'status-done',
+ skipped: 'status-paused',
+ ended: 'status-archived',
}
export function StatusBadge({ kind = 'action', status }) {
diff --git a/frontend/src/constants/status.js b/frontend/src/constants/status.js
index bbff8aa..f9ac8c3 100644
--- a/frontend/src/constants/status.js
+++ b/frontend/src/constants/status.js
@@ -1,10 +1,22 @@
export const INITIATIVE_STATUSES = ['active', 'paused', 'completed', 'archived']
-export const ACTION_STATUSES = ['open', 'in_progress', 'blocked', 'done', 'discarded']
+export const ACTION_STATUSES = [
+ 'open',
+ 'ready',
+ 'in_progress',
+ 'blocked',
+ 'review_required',
+ 'done',
+ 'discarded',
+]
export const BLOCKER_STATUSES = ['open', 'in_progress', 'resolved', 'accepted_risk', 'dismissed']
export const BACKLOG_STATUSES = ['new', 'triaged', 'accepted', 'rejected', 'converted']
export const MILESTONE_STATUSES = ['planned', 'active', 'at_risk', 'reached', 'moved', 'discarded']
+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 PRIORITIES = ['low', 'normal', 'high']
-export const OPEN_ACTION_STATUSES = ['open', 'in_progress', 'blocked']
+export const OPEN_ACTION_STATUSES = ['open', 'ready', 'in_progress', 'blocked', 'review_required']
export const INITIATIVE_STATUS_LABELS = {
active: 'Aktiv',
@@ -15,8 +27,10 @@ export const INITIATIVE_STATUS_LABELS = {
export const ACTION_STATUS_LABELS = {
open: 'Offen',
+ ready: 'Bereit',
in_progress: 'In Arbeit',
blocked: 'Blockiert',
+ review_required: 'Review nötig',
done: 'Erledigt',
discarded: 'Verworfen',
}
@@ -46,6 +60,30 @@ export const MILESTONE_STATUS_LABELS = {
discarded: 'Verworfen',
}
+export const EVIDENCE_STATUS_LABELS = {
+ submitted: 'Eingereicht',
+ accepted: 'Akzeptiert',
+ rejected: 'Abgelehnt',
+}
+
+export const DECISION_STATUS_LABELS = {
+ proposed: 'Vorgeschlagen',
+ decided: 'Entschieden',
+ superseded: 'Ersetzt',
+}
+
+export const REVIEW_STATUS_LABELS = {
+ planned: 'Geplant',
+ completed: 'Abgeschlossen',
+ skipped: 'Übersprungen',
+}
+
+export const RECURRING_STATUS_LABELS = {
+ active: 'Aktiv',
+ paused: 'Pausiert',
+ ended: 'Beendet',
+}
+
export const PRIORITY_LABELS = {
low: 'Niedrig',
normal: 'Normal',
@@ -58,4 +96,8 @@ export const STATUS_LABELS_BY_KIND = {
blocker: BLOCKER_STATUS_LABELS,
backlog: BACKLOG_STATUS_LABELS,
milestone: MILESTONE_STATUS_LABELS,
+ evidence: EVIDENCE_STATUS_LABELS,
+ decision: DECISION_STATUS_LABELS,
+ review: REVIEW_STATUS_LABELS,
+ recurring: RECURRING_STATUS_LABELS,
}
diff --git a/frontend/src/pages/InitiativeDetailPage.jsx b/frontend/src/pages/InitiativeDetailPage.jsx
index acab978..3075dbc 100644
--- a/frontend/src/pages/InitiativeDetailPage.jsx
+++ b/frontend/src/pages/InitiativeDetailPage.jsx
@@ -25,13 +25,41 @@ import {
updateMilestone,
deleteMilestone,
} from '../api/milestones.js'
-import { ACTION_STATUS_LABELS } from '../constants/status.js'
+import {
+ listInitiativeEvidence,
+ createInitiativeEvidence,
+ updateEvidence,
+ deleteEvidence,
+} from '../api/evidence.js'
+import {
+ listInitiativeDecisions,
+ createInitiativeDecision,
+ updateDecision,
+ deleteDecision,
+} from '../api/decisions.js'
+import {
+ listInitiativeReviews,
+ createInitiativeReview,
+ updateReview,
+ deleteReview,
+} from '../api/reviews.js'
+import {
+ listInitiativeRecurring,
+ createInitiativeRecurring,
+ updateRecurring,
+ deleteRecurring,
+} from '../api/recurring.js'
+import { ACTION_STATUSES, ACTION_STATUS_LABELS } from '../constants/status.js'
import { StatusBadge } from '../components/StatusBadge.jsx'
import { PriorityBadge } from '../components/PriorityBadge.jsx'
import { ActionForm } from '../components/ActionForm.jsx'
import { BlockersSection } from '../components/BlockersSection.jsx'
import { BacklogSection } from '../components/BacklogSection.jsx'
import { MilestonesSection } from '../components/MilestonesSection.jsx'
+import { EvidenceSection } from '../components/EvidenceSection.jsx'
+import { DecisionsSection } from '../components/DecisionsSection.jsx'
+import { ReviewsSection } from '../components/ReviewsSection.jsx'
+import { RecurringSection } from '../components/RecurringSection.jsx'
import { EmptyState } from '../components/EmptyState.jsx'
import { ErrorState } from '../components/ErrorState.jsx'
import { LoadingState } from '../components/LoadingState.jsx'
@@ -56,6 +84,10 @@ export function InitiativeDetailPage() {
const [blockers, setBlockers] = useState([])
const [backlogItems, setBacklogItems] = useState([])
const [milestones, setMilestones] = useState([])
+ const [evidenceItems, setEvidenceItems] = useState([])
+ const [decisions, setDecisions] = useState([])
+ const [reviews, setReviews] = useState([])
+ const [recurringItems, setRecurringItems] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [showActionForm, setShowActionForm] = useState(false)
@@ -83,6 +115,10 @@ export function InitiativeDetailPage() {
loads.push(listInitiativeBlockers(id).then(setBlockers).catch(() => setBlockers([])))
loads.push(listInitiativeBacklog(id).then(setBacklogItems).catch(() => setBacklogItems([])))
loads.push(listInitiativeMilestones(id).then(setMilestones).catch(() => setMilestones([])))
+ loads.push(listInitiativeEvidence(id).then(setEvidenceItems).catch(() => setEvidenceItems([])))
+ loads.push(listInitiativeDecisions(id).then(setDecisions).catch(() => setDecisions([])))
+ loads.push(listInitiativeReviews(id).then(setReviews).catch(() => setReviews([])))
+ loads.push(listInitiativeRecurring(id).then(setRecurringItems).catch(() => setRecurringItems([])))
}
await Promise.all(loads)
} catch (err) {
@@ -127,6 +163,8 @@ export function InitiativeDetailPage() {
description: payload.description,
status: payload.status,
priority: payload.priority,
+ due_at: payload.due_at,
+ clear_due_at: !payload.due_at,
})
if (capabilities.has('kairo.action.manage')) {
await setActionAssignments(actionId, payload.assigned_actor_ids || [])
@@ -252,6 +290,126 @@ export function InitiativeDetailPage() {
}
}
+ async function handleCreateEvidence(body) {
+ setFormBusy(true)
+ try {
+ await createInitiativeEvidence(id, body)
+ await load()
+ } catch (err) {
+ setError(err.message)
+ } finally {
+ setFormBusy(false)
+ }
+ }
+
+ async function handleEvidenceStatus(evidenceId, status) {
+ try {
+ await updateEvidence(evidenceId, { status })
+ await load()
+ } catch (err) {
+ setError(err.message)
+ }
+ }
+
+ async function handleDeleteEvidence(evidenceId) {
+ try {
+ await deleteEvidence(evidenceId)
+ await load()
+ } catch (err) {
+ setError(err.message)
+ }
+ }
+
+ async function handleCreateDecision(body) {
+ setFormBusy(true)
+ try {
+ await createInitiativeDecision(id, body)
+ await load()
+ } catch (err) {
+ setError(err.message)
+ } finally {
+ setFormBusy(false)
+ }
+ }
+
+ async function handleDecisionStatus(decisionId, status) {
+ try {
+ await updateDecision(decisionId, { status })
+ await load()
+ } catch (err) {
+ setError(err.message)
+ }
+ }
+
+ async function handleDeleteDecision(decisionId) {
+ try {
+ await deleteDecision(decisionId)
+ await load()
+ } catch (err) {
+ setError(err.message)
+ }
+ }
+
+ async function handleCreateReview(body) {
+ setFormBusy(true)
+ try {
+ await createInitiativeReview(id, body)
+ await load()
+ } catch (err) {
+ setError(err.message)
+ } finally {
+ setFormBusy(false)
+ }
+ }
+
+ async function handleReviewStatus(reviewId, status) {
+ try {
+ await updateReview(reviewId, { status })
+ await load()
+ } catch (err) {
+ setError(err.message)
+ }
+ }
+
+ async function handleDeleteReview(reviewId) {
+ try {
+ await deleteReview(reviewId)
+ await load()
+ } catch (err) {
+ setError(err.message)
+ }
+ }
+
+ async function handleCreateRecurring(body) {
+ setFormBusy(true)
+ try {
+ await createInitiativeRecurring(id, body)
+ await load()
+ } catch (err) {
+ setError(err.message)
+ } finally {
+ setFormBusy(false)
+ }
+ }
+
+ async function handleRecurringStatus(recurringId, status) {
+ try {
+ await updateRecurring(recurringId, { status })
+ await load()
+ } catch (err) {
+ setError(err.message)
+ }
+ }
+
+ async function handleDeleteRecurring(recurringId) {
+ try {
+ await deleteRecurring(recurringId)
+ await load()
+ } catch (err) {
+ setError(err.message)
+ }
+ }
+
if (loading) {
return (
@@ -367,6 +525,11 @@ export function InitiativeDetailPage() {
Zugewiesen: {action.assigned_actor_ids.length} Actor(s)
)}
+ {action.due_at && (
+
+ Fällig: {new Date(action.due_at).toLocaleString('de-DE')}
+
+ )}
@@ -379,7 +542,7 @@ export function InitiativeDetailPage() {
onChange={(e) => handleQuickStatus(action, e.target.value)}
aria-label="Status ändern"
>
- {['open', 'in_progress', 'blocked', 'done', 'discarded'].map((s) => (
+ {ACTION_STATUSES.map((s) => (
@@ -438,6 +601,50 @@ export function InitiativeDetailPage() {
busy={formBusy}
/>
)}
+
+ {capabilities.has('kairo.initiative.read') && (
+
+ )}
+
+ {capabilities.has('kairo.initiative.read') && (
+
+ )}
+
+ {capabilities.has('kairo.initiative.read') && (
+
+ )}
+
+ {capabilities.has('kairo.initiative.read') && (
+
+ )}
)
}