Kairo-Jinkendo/frontend/src/pages/ActionDetailPage.jsx
Lars 6eeddfdc76
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 1m39s
Test Suite / lint-backend (push) Successful in 2s
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 12s
AP1.5: Project, Task und Ausführung als Ist-Kern
Schema 013; Projekte/Arbeitspakete/Aufgaben pflegbar; Tab Zielzustände vs Ausführung.

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

259 lines
7.5 KiB
JavaScript

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'
import { useCapabilities } from '../hooks/useCapabilities.js'
import { useActors } from '../hooks/useActors.js'
function ActionDetailBody({
action,
context,
editing,
onEdit,
onCancelEdit,
onSubmit,
onQuickStatus,
onCreateBlocker,
formBusy,
capabilities,
actorsState,
projects = [],
}) {
if (editing) {
return (
<ActionForm
initial={action}
projects={projects}
actors={actorsState.actors}
actorsLoading={actorsState.loading}
actorsError={actorsState.error}
actorsUsedFallback={actorsState.usedFallback}
onReloadActors={actorsState.reload}
onSubmit={onSubmit}
onCancel={onCancelEdit}
busy={formBusy}
submitLabel="Speichern"
/>
)
}
return (
<>
<ActionHubCard
action={action}
context={context}
editing={false}
onEdit={onEdit}
onCancelEdit={onCancelEdit}
onSubmitEdit={onSubmit}
onQuickStatus={onQuickStatus}
onCreateBlocker={onCreateBlocker}
canManage={capabilities.has('kairo.action.manage')}
canManageBlocker={capabilities.has('kairo.blocker.manage')}
formBusy={formBusy}
actors={actorsState.actors}
actorsLoading={actorsState.loading}
actorsError={actorsState.error}
actorsUsedFallback={actorsState.usedFallback}
onReloadActors={actorsState.reload}
detailMode
/>
<TasksSection
actionId={action.id}
canManage={capabilities.has('kairo.action.manage')}
/>
</>
)
}
function ActionDetailStandalone() {
const { actionId } = useParams()
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)
const [editing, setEditing] = useState(false)
const [formBusy, setFormBusy] = useState(false)
const load = useCallback(async () => {
setLoading(true)
setError(null)
try {
const actionData = await getAction(actionId)
setAction(actionData)
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)
} finally {
setLoading(false)
}
}, [actionId])
useEffect(() => {
load()
}, [load])
async function handleSubmit(payload) {
setFormBusy(true)
try {
await updateAction(action.id, {
title: payload.title,
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(action.id, payload.assigned_actor_ids || [])
}
setEditing(false)
await load()
} catch (err) {
setError(err.message)
} finally {
setFormBusy(false)
}
}
async function handleQuickStatus(status) {
if (!capabilities.has('kairo.action.manage')) return
try {
await updateAction(action.id, { status })
await load()
} catch (err) {
setError(err.message)
}
}
async function handleCreateBlocker() {
if (!capabilities.has('kairo.blocker.manage')) return
setFormBusy(true)
try {
await createInitiativeBlocker(action.initiative_id, {
title: 'Blocker',
action_id: action.id,
set_action_blocked: true,
})
await load()
} catch (err) {
setError(err.message)
} finally {
setFormBusy(false)
}
}
if (loading) {
return (
<div className="app-page">
<LoadingState message="Lade Arbeitspaket …" />
</div>
)
}
if (error || !action) {
return (
<div className="app-page">
<ErrorState message={error || 'Arbeitspaket nicht gefunden.'} onRetry={load} />
</div>
)
}
return (
<div className="app-page">
<nav className="breadcrumb">
<Link to="/workspace">Workspace</Link>
<span aria-hidden="true"> / </span>
<Link to={`/initiatives/${action.initiative_id}`}>Vorhaben</Link>
<span aria-hidden="true"> / </span>
<Link to={`/initiatives/${action.initiative_id}/execution`}>Ausführung</Link>
<span aria-hidden="true"> / </span>
<span>{action.title}</span>
</nav>
<section className="card action-detail-page">
<header className="section-header">
<h2>Arbeitspaket</h2>
</header>
<ActionDetailBody
action={action}
context={context}
editing={editing}
onEdit={() => setEditing(true)}
onCancelEdit={() => setEditing(false)}
onSubmit={handleSubmit}
onQuickStatus={handleQuickStatus}
onCreateBlocker={handleCreateBlocker}
formBusy={formBusy}
capabilities={capabilities}
actorsState={actorsState}
projects={projects}
/>
</section>
</div>
)
}
function ActionDetailNested() {
const { actionId } = useParams()
const ops = useInitiativeOperations()
const action = ops.actions.find((a) => a.id === actionId)
const context = ops.actionContextById[actionId]
const [editing, setEditing] = useState(false)
if (!action) {
return <ErrorState message="Arbeitspaket nicht in diesem Vorhaben." onRetry={ops.reload} />
}
return (
<section className="card action-detail-page">
<header className="section-header">
<h2>Arbeitspaket</h2>
</header>
<ActionDetailBody
action={action}
context={context}
editing={editing}
onEdit={() => setEditing(true)}
onCancelEdit={() => setEditing(false)}
onSubmit={(payload) => ops.handleUpdateAction(action.id, payload)}
onQuickStatus={(status) => ops.handleQuickStatus(action, status)}
onCreateBlocker={() => ops.handleCreateBlockerForAction(action.id)}
formBusy={ops.formBusy}
capabilities={ops.capabilities}
actorsState={{
actors: ops.actors,
loading: ops.actorsLoading,
error: ops.actorsError,
usedFallback: ops.actorsUsedFallback,
reload: ops.reloadActors,
}}
projects={ops.projects}
/>
</section>
)
}
export function ActionDetailPage({ nested = false }) {
if (nested) {
return <ActionDetailNested />
}
return <ActionDetailStandalone />
}