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
Schema 013; Projekte/Arbeitspakete/Aufgaben pflegbar; Tab Zielzustände vs Ausführung. Co-authored-by: Cursor <cursoragent@cursor.com>
259 lines
7.5 KiB
JavaScript
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 />
|
|
}
|