Kairo-Jinkendo/frontend/src/context/InitiativeOperationsContext.jsx
Lars 7e4936a2e4
Some checks failed
Deploy Development / deploy (push) Failing after 37s
Test Suite / pytest-backend (push) Successful in 1m41s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / compose-smoke (push) Has been skipped
Test Suite / k6 /api/health Baseline (push) Successful in 18s
Test Suite / playwright-smoke (push) Successful in 13s
AP1.6: Plan/Ist-Verknuepfung und Journey-Timeline.
Backlog, Actions, Projects und Tasks koennen Gates zugeordnet werden; Gate-Beitragsliste und chronologische Journey machen Plan/Ist nachvollziehbar.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 08:43:11 +02:00

669 lines
17 KiB
JavaScript

import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from 'react'
import { useParams } from 'react-router-dom'
import {
getInitiative,
getInitiativeSteeringSnapshot,
listInitiativeActions,
createInitiativeAction,
} from '../api/initiatives.js'
import { updateAction, setActionAssignments } from '../api/actions.js'
import {
listInitiativeBlockers,
createInitiativeBlocker,
updateBlocker,
deleteBlocker,
} from '../api/blockers.js'
import {
listInitiativeBacklog,
createInitiativeBacklogItem,
updateBacklogItem,
deleteBacklogItem,
convertBacklogToAction,
} from '../api/backlog.js'
import {
listInitiativeRoadmapItems,
createInitiativeRoadmapItem,
updateRoadmapItem,
deleteRoadmapItem,
verifyRoadmapItemReached,
} from '../api/roadmap.js'
import {
listInitiativeProjects,
createInitiativeProject,
deleteProject,
} from '../api/projects.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 { listSteeringMethods, updateInitiativeSteeringMethod } from '../api/steering.js'
import { useCapabilities } from '../hooks/useCapabilities.js'
import { useActors } from '../hooks/useActors.js'
import { useSession } from './SessionContext.jsx'
import { collectProjectSubtreeIds } from '../utils/projectTree.js'
const InitiativeOperationsContext = createContext(null)
export function InitiativeOperationsProvider({ children }) {
const { id } = useParams()
const { context, loading: sessionLoading } = useSession()
const { capabilities } = useCapabilities()
const actorsState = useActors()
const [initiative, setInitiative] = useState(null)
const [actions, setActions] = useState([])
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([])
const [recurringItems, setRecurringItems] = useState([])
const [steeringSnapshot, setSteeringSnapshot] = useState(null)
const [steeringSnapshotLoading, setSteeringSnapshotLoading] = useState(false)
const [steeringSnapshotError, setSteeringSnapshotError] = useState(null)
const [steeringMethods, setSteeringMethods] = useState([])
const [methodBusy, setMethodBusy] = useState(false)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [showActionForm, setShowActionForm] = useState(false)
const [editingAction, setEditingAction] = useState(null)
const [formBusy, setFormBusy] = useState(false)
const [hideDone, setHideDone] = useState(true)
const load = useCallback(async () => {
if (sessionLoading || !id) return
setLoading(true)
setError(null)
try {
const [initData, actionData] = await Promise.all([
getInitiative(id),
listInitiativeActions(id),
])
setInitiative(initData)
setActions(actionData)
const loads = []
const canReadOm =
capabilities.has('kairo.initiative.read') ||
capabilities.has('kairo.blocker.read')
if (canReadOm) {
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([])))
loads.push(listInitiativeRecurring(id).then(setRecurringItems).catch(() => setRecurringItems([])))
loads.push(
listSteeringMethods().then(setSteeringMethods).catch(() => setSteeringMethods([]))
)
loads.push(
(async () => {
setSteeringSnapshotLoading(true)
setSteeringSnapshotError(null)
try {
setSteeringSnapshot(await getInitiativeSteeringSnapshot(id))
} catch (err) {
setSteeringSnapshot(null)
setSteeringSnapshotError(err.message || 'Snapshot fehlgeschlagen')
} finally {
setSteeringSnapshotLoading(false)
}
})()
)
}
await Promise.all(loads)
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}, [id, capabilities, sessionLoading])
useEffect(() => {
load()
}, [load])
const selectedLeafProjectId = useMemo(() => {
if (!selectedProjectId) return ''
const match = projects.find((p) => p.id === selectedProjectId)
if (match?.is_leaf) return selectedProjectId
return ''
}, [selectedProjectId, projects])
const visibleActions = useMemo(
() => {
let list = hideDone
? actions.filter((a) => a.status !== 'done' && a.status !== 'discarded')
: actions
if (selectedProjectId) {
const subtreeIds = collectProjectSubtreeIds(projects, selectedProjectId)
list = list.filter((a) => a.project_id && subtreeIds.has(a.project_id))
}
return list
},
[actions, hideDone, selectedProjectId, projects]
)
const actionContextById = useMemo(
() => Object.fromEntries((steeringSnapshot?.actions || []).map((a) => [a.id, a])),
[steeringSnapshot]
)
const unlinkedBlockers = useMemo(
() => blockers.filter((b) => !b.action_id),
[blockers]
)
async function handleMethodChange(methodKey) {
setMethodBusy(true)
try {
await updateInitiativeSteeringMethod(id, methodKey)
setSteeringSnapshot(await getInitiativeSteeringSnapshot(id))
} catch (err) {
setSteeringSnapshotError(err.message)
} finally {
setMethodBusy(false)
}
}
async function handleCreateAction(payload) {
setFormBusy(true)
try {
const assigned =
payload.assigned_actor_ids?.length > 0
? payload.assigned_actor_ids
: context?.actor?.id
? [context.actor.id]
: []
await createInitiativeAction(id, {
...payload,
project_id: payload.project_id || selectedLeafProjectId || undefined,
assigned_actor_ids: assigned,
})
setShowActionForm(false)
await load()
} catch (err) {
setError(err.message)
} finally {
setFormBusy(false)
}
}
async function handleUpdateAction(actionId, payload) {
setFormBusy(true)
try {
await updateAction(actionId, {
title: payload.title,
description: payload.description,
status: payload.status,
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,
roadmap_item_id: payload.roadmap_item_id || undefined,
clear_roadmap_item: payload.roadmap_item_id === '' || payload.roadmap_item_id === null,
})
if (capabilities.has('kairo.action.manage')) {
await setActionAssignments(actionId, payload.assigned_actor_ids || [])
}
setEditingAction(null)
await load()
} catch (err) {
setError(err.message)
} finally {
setFormBusy(false)
}
}
async function handleQuickStatus(action, status) {
if (!capabilities.has('kairo.action.manage')) return
try {
await updateAction(action.id, { status })
await load()
} catch (err) {
setError(err.message)
}
}
async function handleCreateBlocker(body) {
setFormBusy(true)
try {
await createInitiativeBlocker(id, body)
await load()
} catch (err) {
setError(err.message)
} finally {
setFormBusy(false)
}
}
async function handleCreateBlockerForAction(actionId) {
if (!capabilities.has('kairo.blocker.manage')) return
setFormBusy(true)
try {
await createInitiativeBlocker(id, {
title: 'Blocker',
action_id: actionId,
set_action_blocked: true,
})
await load()
} catch (err) {
setError(err.message)
} finally {
setFormBusy(false)
}
}
async function handleBlockerStatus(blockerId, status) {
try {
await updateBlocker(blockerId, { status })
await load()
} catch (err) {
setError(err.message)
}
}
async function handleDeleteBlocker(blockerId) {
try {
await deleteBlocker(blockerId)
await load()
} catch (err) {
setError(err.message)
}
}
async function handleCreateBacklog(body) {
setFormBusy(true)
try {
await createInitiativeBacklogItem(id, body)
await load()
} catch (err) {
setError(err.message)
} finally {
setFormBusy(false)
}
}
async function handleBacklogGate(itemId, roadmapItemId) {
try {
await updateBacklogItem(itemId, {
roadmap_item_id: roadmapItemId || undefined,
clear_roadmap_item: !roadmapItemId,
})
await load()
} catch (err) {
setError(err.message)
}
}
async function handleBacklogStatus(itemId, status) {
try {
await updateBacklogItem(itemId, { status })
await load()
} catch (err) {
setError(err.message)
}
}
async function handleConvertBacklog(itemId) {
setFormBusy(true)
try {
await convertBacklogToAction(itemId)
await load()
} catch (err) {
setError(err.message)
} finally {
setFormBusy(false)
}
}
async function handleDeleteBacklog(itemId) {
try {
await deleteBacklogItem(itemId)
await load()
} catch (err) {
setError(err.message)
}
}
async function handleCreateRoadmapItem(body) {
setFormBusy(true)
try {
await createInitiativeRoadmapItem(id, body)
await load()
} catch (err) {
setError(err.message)
} finally {
setFormBusy(false)
}
}
async function handleRoadmapItemStatus(itemId, status) {
try {
await updateRoadmapItem(itemId, { status })
await load()
} catch (err) {
setError(err.message)
}
}
async function handleUpdateRoadmapItem(itemId, body) {
try {
await updateRoadmapItem(itemId, body)
await load()
} catch (err) {
setError(err.message)
}
}
async function handleVerifyWithEvidence(itemId, evidenceTitle) {
setFormBusy(true)
setError(null)
try {
await createInitiativeEvidence(id, {
title: evidenceTitle,
roadmap_item_id: itemId,
status: 'accepted',
})
await verifyRoadmapItemReached(itemId)
await load()
} catch (err) {
setError(err.message)
} finally {
setFormBusy(false)
}
}
async function handleVerifyRoadmapItem(itemId) {
setFormBusy(true)
try {
await verifyRoadmapItemReached(itemId)
await load()
} catch (err) {
setError(err.message)
} finally {
setFormBusy(false)
}
}
async function handleDeleteRoadmapItem(itemId) {
try {
await deleteRoadmapItem(itemId)
await load()
} catch (err) {
setError(err.message)
}
}
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]
)
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)
}
}
const value = {
initiativeId: id,
initiative,
actions,
visibleActions,
blockers,
unlinkedBlockers,
backlogItems,
roadmapItems,
projects,
selectedProjectId,
setSelectedProjectId,
milestones,
evidenceItems,
decisions,
reviews,
recurringItems,
steeringSnapshot,
steeringSnapshotLoading,
steeringSnapshotError,
steeringMethods,
methodBusy,
loading,
error,
showActionForm,
setShowActionForm,
editingAction,
setEditingAction,
formBusy,
hideDone,
setHideDone,
actionContextById,
capabilities,
actors: actorsState.actors,
actorsLoading: actorsState.loading,
actorsError: actorsState.error,
actorsUsedFallback: actorsState.usedFallback,
reloadActors: actorsState.reload,
reload: load,
handleMethodChange,
handleCreateAction,
handleUpdateAction,
handleQuickStatus,
handleCreateBlocker,
handleCreateBlockerForAction,
handleBlockerStatus,
handleDeleteBlocker,
handleCreateBacklog,
handleBacklogGate,
handleBacklogStatus,
handleConvertBacklog,
handleDeleteBacklog,
handleCreateRoadmapItem,
handleRoadmapItemStatus,
handleUpdateRoadmapItem,
handleVerifyRoadmapItem,
handleVerifyWithEvidence,
handleDeleteRoadmapItem,
handleCreateMilestone: handleCreateRoadmapItem,
handleMilestoneStatus: handleRoadmapItemStatus,
handleDeleteMilestone: handleDeleteRoadmapItem,
handleCreateProject,
handleDeleteProject,
handleCreateEvidence,
handleEvidenceStatus,
handleDeleteEvidence,
handleCreateDecision,
handleDecisionStatus,
handleDeleteDecision,
handleCreateReview,
handleReviewStatus,
handleDeleteReview,
handleCreateRecurring,
handleRecurringStatus,
handleDeleteRecurring,
}
return (
<InitiativeOperationsContext.Provider value={value}>
{children}
</InitiativeOperationsContext.Provider>
)
}
export function useInitiativeOperations() {
const ctx = useContext(InitiativeOperationsContext)
if (!ctx) {
throw new Error('useInitiativeOperations must be used within InitiativeOperationsProvider')
}
return ctx
}