import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from 'react' import { useParams } from 'react-router-dom' import { getInitiative, getInitiativeSteeringSnapshot, listInitiativeActions, createInitiativeAction, updateInitiative, } from '../api/initiatives.js' import { updateAction, setActionAssignments } from '../api/actions.js' import { saveInitiativeDynamicFields, splitInitiativeFormPayload } from '../api/entityFields.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, updateProject, 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, updateInitiativeMethodProfile, } from '../api/steering.js' import { listInitiativeWorkCycles, getActiveWorkCycle, createWorkCycle, activateWorkCycle, } from '../api/workCycles.js' import { useCapabilities } from '../hooks/useCapabilities.js' import { useActors } from '../hooks/useActors.js' import { useSession } from './SessionContext.jsx' import { useOptionalProgramScope } from './ProgramScopeContext.jsx' import { collectProjectSubtreeIds } from '../utils/projectTree.js' const InitiativeOperationsContext = createContext(null) export function InitiativeOperationsProvider({ children, initiativeId: initiativeIdProp }) { const { id: routeId } = useParams() const scope = useOptionalProgramScope() const id = initiativeIdProp || routeId || scope?.initiativeId || null 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 [workCycles, setWorkCycles] = useState([]) const [activeWorkCycle, setActiveWorkCycle] = useState(null) 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(listInitiativeWorkCycles(id).then(setWorkCycles).catch(() => setWorkCycles([]))) loads.push( getActiveWorkCycle(id) .then((item) => setActiveWorkCycle(item || null)) .catch(() => setActiveWorkCycle(null)) ) 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 sprintActions = useMemo(() => { if (!activeWorkCycle?.id) return [] const cycleId = activeWorkCycle.id let list = hideDone ? actions.filter((a) => a.status !== 'done' && a.status !== 'discarded') : actions list = list.filter((a) => a.work_cycle_id === cycleId) if (selectedProjectId) { const subtreeIds = collectProjectSubtreeIds(projects, selectedProjectId) list = list.filter((a) => a.project_id && subtreeIds.has(a.project_id)) } return list }, [actions, hideDone, activeWorkCycle?.id, 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 handleMethodProfileChange(methodProfileKey) { setMethodBusy(true) try { await updateInitiativeMethodProfile(id, methodProfileKey || null) 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, work_cycle_id: payload.work_cycle_id || undefined, clear_work_cycle: payload.work_cycle_id === '' || payload.work_cycle_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 handleUpdateInitiative(body) { setFormBusy(true) const { core, dynamicFields } = splitInitiativeFormPayload(body) try { await updateInitiative(id, core) await saveInitiativeDynamicFields(id, dynamicFields) await load() return true } catch (err) { setError(err.message) return false } finally { setFormBusy(false) } } async function handleCreateBacklog(body) { setFormBusy(true) try { await createInitiativeBacklogItem(id, body) await load() } catch (err) { setError(err.message) } finally { setFormBusy(false) } } async function handleUpdateBacklog(itemId, body) { setFormBusy(true) try { await updateBacklogItem(itemId, body) await load() return true } catch (err) { setError(err.message) return false } 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, { assign_active_sprint: true }) await load() } catch (err) { setError(err.message) } finally { setFormBusy(false) } } async function handleCreateWorkCycle(body) { setFormBusy(true) try { await createWorkCycle(id, body) await load() } catch (err) { setError(err.message) } finally { setFormBusy(false) } } async function handleActivateWorkCycle(workCycleId) { setFormBusy(true) try { await activateWorkCycle(id, workCycleId) 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 { const created = await createInitiativeRoadmapItem(id, body) await load() return created } catch (err) { setError(err.message) throw err } 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) setError(null) try { const created = await createInitiativeProject(id, body) try { setProjects(await listInitiativeProjects(id)) } catch (refreshErr) { setError( `Projekt angelegt, aber die Liste konnte nicht aktualisiert werden: ${refreshErr.message}`, ) return created } return created } catch (err) { setError(err.message) return null } finally { setFormBusy(false) } } async function handleUpdateProject(projectId, body) { setFormBusy(true) setError(null) try { await updateProject(projectId, body) setProjects(await listInitiativeProjects(id)) return true } catch (err) { setError(err.message) return false } finally { setFormBusy(false) } } async function handleDeleteProject(projectId) { try { await deleteProject(projectId) if (selectedProjectId === projectId) setSelectedProjectId('') setProjects(await listInitiativeProjects(id)) return true } catch (err) { setError(err.message) return false } } async function handleReorderProjects(patches) { if (!patches?.length) return true setFormBusy(true) setError(null) try { await Promise.all( patches.map((patch) => updateProject(patch.id, { sort_order: patch.sort_order })), ) setProjects(await listInitiativeProjects(id)) return true } catch (err) { setError(err.message) return false } finally { setFormBusy(false) } } async function handleReorderBacklog(patches) { if (!patches?.length) return true setFormBusy(true) setError(null) try { await Promise.all( patches.map((patch) => updateBacklogItem(patch.id, { sort_order: patch.sort_order })), ) await load() return true } catch (err) { setError(err.message) return false } finally { setFormBusy(false) } } async function handleReorderRoadmapItems(patches) { if (!patches?.length) return true setFormBusy(true) setError(null) try { await Promise.all( patches.map((patch) => updateRoadmapItem(patch.id, { sort_order: patch.sort_order })), ) await load() return true } catch (err) { setError(err.message) return false } finally { setFormBusy(false) } } 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, workCycles, activeWorkCycle, sprintActions, 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, handleMethodProfileChange, handleCreateAction, handleUpdateAction, handleQuickStatus, handleCreateBlocker, handleCreateBlockerForAction, handleBlockerStatus, handleDeleteBlocker, handleCreateBacklog, handleUpdateBacklog, handleUpdateInitiative, handleBacklogGate, handleBacklogStatus, handleConvertBacklog, handleCreateWorkCycle, handleActivateWorkCycle, handleDeleteBacklog, handleCreateRoadmapItem, handleRoadmapItemStatus, handleUpdateRoadmapItem, handleVerifyRoadmapItem, handleVerifyWithEvidence, handleDeleteRoadmapItem, handleCreateMilestone: handleCreateRoadmapItem, handleMilestoneStatus: handleRoadmapItemStatus, handleDeleteMilestone: handleDeleteRoadmapItem, handleCreateProject, handleUpdateProject, handleDeleteProject, handleReorderProjects, handleReorderBacklog, handleReorderRoadmapItems, handleCreateEvidence, handleEvidenceStatus, handleDeleteEvidence, handleCreateDecision, handleDecisionStatus, handleDeleteDecision, handleCreateReview, handleReviewStatus, handleDeleteReview, handleCreateRecurring, handleRecurringStatus, handleDeleteRecurring, } return ( {children} ) } export function useInitiativeOperations() { const ctx = useContext(InitiativeOperationsContext) if (!ctx) { throw new Error('useInitiativeOperations must be used within InitiativeOperationsProvider') } return ctx }